关于本书
本书中的所有代码示例均可在Manning出版社的官方网站(https://www.manning.com/books/build-a-large-language-model-from-scratch)和GitHub(https://github.com/rasbt/LLMs-from-scratch)上找到
第2章 处理文本数据
2.4 引入特殊上下文词元
• [PAD](填充):当使用批次大小(batch size)大于1的批量数据训练大语言模型时,数据中的文本长度可能不同。为了使所有文本具有相同的长度,较短的文本会通过添加[PAD]词元进行扩展或“填充”,以匹配批量数据中的最长文本的长度。
2.5 BPE
由于BPE的实现相对复杂,因此我们将使用现有的Python开源库tiktoken,它基于Rust的源代码非常高效地实现了BPE算法。
BPE算法的原理是将不在预定义词汇表中的单词分解为更小的子词单元甚至单个字符,从而能够处理词汇表之外的单词。
2.7 创建词元嵌入
嵌入层实质上执行的是一种查找操作,它根据词元ID从嵌入层的权重矩阵中检索出相应的行。
注意 如果你熟悉独热编码(one-hot encoding),那么本质上可以将嵌入层方法视为一种更有效的实现独热编码的方法。它先进行独热编码,然后在全连接层中进行矩阵乘法,这在本书的补充代码中有所说明。由于嵌入层只是独热编码和矩阵乘法方法的一种更高效的实现,因此它可以被视为一个能够通过反向传播进行优化的神经网络层。
2.8 编码单词位置信息
大语言模型存在一个小缺陷——它们的自注意力机制(参见第3章)无法感知词元在序列中的位置或顺序。嵌入层的工作机制是,无论词元ID在输入序列中的位置如何,相同的词元ID始终被映射到相同的向量表示
原则上,带有确定性且与位置无关的词元ID嵌入能够提升其可再现性。然而,由于大语言模型的自注意力机制本质上与位置无关,因此向模型中注入额外的位置信息是有帮助的。
为了实现这一点,可以采用两种位置信息嵌入策略:绝对位置嵌入和相对位置嵌入。
绝对位置嵌入(absolute positional embedding)直接与序列中的特定位置相关联。对于输入序列的每个位置,该方法都会向对应词元的嵌入向量中添加一个独特的位置嵌入,以明确指示其在序列中的确切位置。
相对位置嵌入(relative positional embedding)关注的是词元之间的相对位置或距离,而非它们的绝对位置。这意味着模型学习的是词元之间的“距离”关系,而不是它们在序列中的“具体位置”。这种方法使得模型能够更好地适应不同长度(包括在训练过程中从未见过的长度)的序列。
以上两种位置嵌入都旨在提升大语言模型对词元顺序及其相互关系的理解能力,从而实现更准确、更具上下文感知力的预测。选择使用哪种嵌入策略,通常取决于具体的应用场景和数据特性。
第3章 编码注意力机制
3.1 长序列建模中的问题
编码器-解码器RNN的一个主要限制是,在解码阶段,RNN无法直接访问编码器中的早期隐藏状态。因此,它只能依赖当前的隐藏状态,这个状态包含了所有相关信息。这可能导致上下文丢失,特别是在复杂句子中,依赖关系可能跨越较长的距离。
3.2 使用注意力机制捕捉数据依赖关系
自注意力是Transformer模型中的一种机制,它通过允许一个序列中的每个位置与同一序列中的其他所有位置进行交互并权衡其重要性,来计算出更高效的输入表示。
3.3 通过自注意力机制关注输入的不同部分
在自注意力机制中,“自”指的是该机制通过关联单个输入序列中的不同位置来计算注意力权重的能力。它可以评估并学习输入本身各个部分之间的关系和依赖,比如句子中的单词或图像中的像素。
这与传统的注意力机制形成对比。传统的注意力机制关注的是两个不同序列元素之间的关系,比如在序列到序列模型中,注意力可能在输入序列和输出序列之间
点积不仅被视为一种将两个向量转化为标量值的数学工具,而且也是度量相似度的一种方式,因为它可以量化两个向量之间的对齐程度:点积越大,向量之间的对齐程度或相似度就越高。
在自注意机制中,点积决定了序列中每个元素对其他元素的关注程度:点积越大,两个元素之间的相似度和注意力分数就越高。
归一化的主要目的是获得总和为1的注意力权重。这种归一化是一个惯例,有助于解释结果,并能维持大语言模型的训练稳定性。
在实际应用中,使用softmax函数进行归一化更为常见,而且是一种更可取的做法。这种方法更好地处理了极值,并在训练期间提供了更有利的梯度特性。
softmax函数可以保证注意力权重总是正值,这使得输出可以被解释为概率或相对重要性,其中权重越高表示重要程度越高。
在计算前面的注意力分数张量时,我们使用了Python中的for循环。然而,for循环通常较慢,因此可以使用矩阵乘法来得到相同的结果:
attn_scores = inputs @ inputs.T print(attn_scores)
在使用PyTorch时,像torch.softmax这样的函数中的dim参数用于指定输入张量的计算维度。将dim设置为-1表示让softmax函数在attn_scores张量的最后一个维度上进行归一化。如果attn_scores是一个二维张量(比如形状为[行, 列]),那么它将对列进行归一化,使得每行的值(在列维度上的总和)为1。
3.4 实现带可训练权重的自注意力机制
自注意机制
也被称为缩放点积注意力(scaled dot-product attention)。
权重参数是定义网络连接的基本学习系数,而注意力权重是动态且特定于上下文的值。
通过将注意力分数除以键向量的嵌入维度的平方根来进行缩放(取平方根在数学上等同于以0.5为指数进行幂运算)。
通过嵌入维度的平方根进行缩放解释了为什么这种自注意力机制也被称为缩放点积注意力机制。
3.5 利用因果注意力隐藏未来词汇
因果注意力(也称为掩码注意力)是一种特殊的自注意力形式。它限制模型在处理任何给定词元时,只能基于序列中的先前和当前输入来计算注意力分数,而标准的自注意力机制可以一次性访问整个输入序列。
在Transformer架构中,一些包括GPT在内的模型通常会在两个特定时间点使用注意力机制中的dropout:一是计算注意力权重之后,二是将这些权重应用于值向量之后。
在对注意力权重矩阵应用50%的dropout率时,矩阵中有一半的元素会随机被置为0。为了补偿减少的活跃元素,矩阵中剩余元素的值会按1/0.5=2的比例进行放大。这种放大对于维持注意力权重的整体平衡非常重要,可以确保在训练和推理过程中,注意力机制的平均影响保持一致。
3.6 将单头注意力扩展到多头注意力
“多头”这一术语指的是将注意力机制分成多个“头”,每个“头”独立工作。在这种情况下,单个因果注意力模块可以被看作单头注意力,因为它只有一组注意力权重按顺序处理输入。
多头注意力的主要思想是多次(并行)运行注意力机制,每次使用学到的不同的线性投影——这些投影是通过将输入数据(比如注意力机制中的查询向量、键向量和值向量)乘以权重矩阵得到的。
第4章 从头实现GPT模型进行文本生成
4.2 使用层归一化进行归一化激活
层归一化的主要思想是调整神经网络层的激活(输出),使其均值为0且方差(单位方差)为1。这种调整有助于加速权重的有效收敛,并确保训练过程的一致性和可靠性。
非线性激活函数ReLU(修正线性单元),ReLU是神经网络中的一种标准激活函数。如果你不熟悉ReLU,可以这样来理解:它只是简单地将负输入值设为0,从而确保层的输出值都是正值
对之前得到的层输出进行层归一化操作。具体方法是减去均值,并将结果除以方差的平方根(也就是标准差)
这个层归一化的具体实现作用在输入张量x的最后一个维度上,该维度对应于嵌入维度(emb_dim)。变量eps是一个小常数(epsilon),在归一化过程中会被加到方差上以防止除零错误。scale和shift是两个可训练的参数(与输入维度相同),如果在训练过程中发现调整它们可以改善模型的训练任务表现,那么大语言模型会自动进行调整。这使得模型能够学习适合其数据处理的最佳缩放和偏移。
与在批次维度上进行归一化的批归一化不同,层归一化是在特征维度上进行归一化。
由于层归一化是对每个输入独立进行归一化,不受批次大小的限制,因此在这些场景中它提供了更多的灵活性和稳定性。这在分布式训练或在资源受限的环境中部署模型时尤为重要。
4.3 实现具有GELU激活函数的前馈神经网络
在大语言模型中,除了传统的ReLU,还有其他几种激活函数,其中两个值得注意的例子是GELU(Gaussian Error Linear Unit)和SwiGLU(Swish-gated Linear Unit)。
GELU和SwiGLU是更为复杂且平滑的激活函数,分别结合了高斯分布和sigmoid门控线性单元。与较为简单的ReLU激活函数相比,它们能够提升深度学习模型的性能。
4.4 添加快捷连接
快捷连接(也称为“跳跃连接”或“残差连接”)
快捷连接最初用于计算机视觉中的深度网络(特别是残差网络),目的是缓解梯度消失问题。梯度消失问题指的是在训练过程中,梯度在反向传播时逐渐变小,导致早期网络层难以有效训练。
快捷连接通过跳过一个或多个层,为梯度在网络中的流动提供了一条可替代且更短的路径。这是通过将一层的输出添加到后续层的输出中实现的。这也是为什么这种连接被称为跳跃连接。在反向传播训练中,它们在维持梯度流动方面扮演着至关重要的角色。
4.5 连接Transformer块中的注意力层和线性层
4.6 实现GPT模型
原始GPT-2架构中使用了一个叫作权重共享(weight tying)的概念。也就是说,原始GPT-2架构是将词元嵌入层作为输出层重复使用的。
第5章 在无标签数据上进行预训练
5.1 评估文本生成模型
在深度学习中,通常的做法不是将平均对数概率升至0,而是将负平均对数概率降至0。负平均对数概率就是平均对数概率乘以-1
在深度学习中,将-10.7940这个负值转换为10.7940的术语称为交叉熵损失。
在机器学习和深度学习中,交叉熵损失是一种常用的度量方式,用于衡量两个概率分布之间的差异——通常是标签(在这里是数据集中的词元)的真实分布和模型生成的预测分布(例如,由大语言模型生成的词元概率)之间的差异。
在实践中,“交叉熵”和“负平均对数概率”这两个术语是相关的,且经常可以互换使用。
先前,我们应用softmax函数,选择了与目标ID对应的概率分数,并计算了负对数概率的平均值。PyTorch的cross_entropy函数将为我们处理所有这些步骤
困惑度通常与交叉熵损失一起用来评估模型在诸如语言建模等任务中的性能。它可以提供一种更易解释的方式来理解模型在预测序列中的下一个词元时的不确定性。
困惑度可以衡量模型预测的概率分布与数据集中实际词汇分布的匹配程度。与损失类似,较低的困惑度表明模型的预测更接近实际分布。
困惑度可以通过perplexity = torch.exp(loss)计算得出
困惑度通常被认为比原始损失值更易于解释,因为它表示模型在每一步中对于有效词汇量的不确定性。
5.3 控制随机性的解码策略
为了实现一个概率采样过程,现在可以用PyTorch中的multinomial函数替换argmax
通过一个称为温度缩放的概念,可以进一步控制分布和选择过程。温度缩放指的是将logits除以一个大于0的数
通过与概率采样和温度缩放相结合,Top-k采样可以改善文本生成结果。在采样中,可以将采样的词元限制在前个最可能的词元上,并通过掩码概率分数的方式来排除其他词元
方法用负无穷值(-inf)替换所有未选择的logits,因此在计算softmax值时,非前词元的概率分数为0,剩余的概率总和为1。
5.4 使用PyTorch加载和保存模型权重
可以使用torch.save保存模型和优化器的state_dict内容:
torch.save({ "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), }, "model_and_optimizer.pth" )
然后,可以先使用torch.load加载保存的数据,再使用load_state_dict方法来恢复模型和优化器的状态。
checkpoint = torch.load("model_and_optimizer.pth", map_location=device) model = GPTModel(GPT_CONFIG_124M) model.load_state_dict(checkpoint["model_state_dict"]) optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1) optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) model.train();
5.6 小结
• 默认情况下,下一个词元是通过将模型输出转换为概率分数,并从词汇表中选择与最高概率分数对应的词元来生成的,这被称为“贪婪解码”。
• 通过使用概率采样和温度缩放,可以干预生成文本的多样性和连贯性。
第6章 针对分类的微调
6.3 创建数据加载器
6.5 添加分类头
在基于神经网络的语言模型中,较低层通常捕捉基本的语言结构和语义,适用于广泛的任务和数据集,最后几层(靠近输出的层)更侧重于捕捉细微的语言模式和特定任务的特征。因此,只微调最后几层通常就足以将模型适应到新任务。同时,仅微调少量层在计算上也更加高效。
根据图6-12中的因果注意力掩码设置,序列中的最后一个词元累积了最多的信息,因为它是唯一一个可以访问之前所有数据的词元。因此,在垃圾消息分类任务中,我们在微调过程中会关注这个最后的词元。
第7章 通过微调遵循人类指令
7.3 将数据组织成训练批次
值得注意的是,我们在目标列表中保留了一个结束符词元,ID为50256,如图7-12所示。保留此词元有助于大语言模型学会何时根据指令生成结束符词元,一般我们将其作为生成的回复已经完成的指示符。

在PyTorch中,交叉熵函数的默认设置为cross_entropy(…, ignore_index=-100)。这意味着它会忽略标记为-100的目标。
7.5 加载预训练的大语言模型
较小的模型在学习高质量的指令遵循任务时,缺乏执行该任务所需的复杂模式和细微行为的能力。
附录A PyTorch简介
要在PyTorch中使用Apple Silicon芯片,首先需要按常规安装PyTorch。然后,可以在Python中运行一个简单的代码片段,以检查你的Mac是否支持使用Apple Silicon芯片加速PyTorch:
print(torch.backends.mps.is_available())
PyTorch中常用的矩阵相乘方法是.matmul方法:
print(tensor2d.matmul(tensor2d.T))
也可以使用@运算符,它能够更简洁地实现相同的功能:
print(tensor2d @ tensor2d.T)
计算图是一种有向图,主要用于表达和可视化数学表达式。在深度学习的背景下,计算图列出了计算神经网络输出所需的计算顺序——我们需要用它来计算反向传播所需的梯度,这是神经网络的主要训练算法。
实际上,PyTorch在后台构建了这样一个计算图,我们可以利用它来计算损失函数相对于模型参数(这里是和)的梯度,从而训练模型。
偏导数,它测量的是一个函数相对于其中一个变量变化的速率。
梯度是一个向量,包含了一个多变量函数(输入变量超过一个的函数)的所有偏导数。

PyTorch提供了更高级的工具来自动化这个过程。例如,我们可以对损失函数调用.backward方法,随后PyTorch将计算计算图中所有叶节点的梯度,这些梯度将通过张量的.grad属性进行存储:loss.backward() print(w1.grad) print(b.grad)
Addmm代表的是矩阵乘法(mm)后接加法(Add)的组合运算。
如果只想使用网络进行预测而不进行训练或反向传播(比如在训练之后使用它进行预测),那么为反向传播构建这个计算图可能会浪费资源,因为它会执行不必要的计算并消耗额外的内存。因此,当使用模型进行推理(比如做出预测)而不是训练时,最好的做法是使用torch.no_grad()上下文管理器。这会告诉PyTorch无须跟踪梯度,从而可以显著节省内存和计算资源:
with torch.no_grad(): out = model(X) print(out)
在PyTorch中,通常的做法是让模型返回最后一层的输出(logits),而不将这些输出传递给非线性激活函数。这是因为PyTorch常用的损失函数会将softmax(或二分类时的sigmoid)操作与负对数似然损失结合在一个类中。这样做是为了提高数值计算的效率和稳定性。因此,如果想为预测结果计算类别成员概率,那么就需要显式调用softmax函数:
with torch.no_grad(): out = torch.softmax(model(X), dim=1) print(out)这将打印以下内容:
tensor([[0.3113, 0.3934, 0.2952]]))现在这些值可以解释为类别成员的概率,并且它们的总和大约为1。对于这个随机输入,这些值大致相等,这是未经过训练的随机初始化模型的预期结果。
注意 PyTorch要求类别标签从标签0开始,并且最大的类别标签值不得超过输出节点数减1(因为Python的索引从0开始)。因此,如果我们有类别标签0、1、2、3和4,那么神经网络的输出层应包含5个节点。
在实践中,如果一个训练轮次的最后一个批次显著小于其他批次,那么可能会影响训练过程中的收敛。为此,可以设置drop_last=True,这将在每轮中丢弃最后一个批次
model.train()和model.eval() … 用于将模型置于训练模式或评估模式。
注意 为了避免不必要的梯度累积,确保在每次更新中调用optimizer.zero_grad()来将梯度重置为0,这很重要。否则,梯度会逐渐累积起来,这往往是我们不愿意见到的。
如果一个PyTorch张量存放在某个设备上,那么其操作也会在同一个设备上执行。
可以使用.to()方法。这个方法与我们用来更改张量数据类型的方法(参见A.2.2节)相同,它能够将这些张量转移到GPU上并在那里执行加法操作:
tensor_1 = tensor_1.to("cuda") tensor_2 = tensor_2.to("cuda") print(tensor_1 + tensor_2)
如果你的机器有多个GPU,那么可以指定要将张量转移到哪个GPU上。这可以通过在传输命令中指定设备ID来实现,比如使用.to(“cuda:0”)、.to(“cuda:1”)等命令。
这被认为是分享PyTorch代码时的最佳实践:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device = torch.device( "mps" if torch.backends.mps.is_available() else "cpu" )
在Jupyter Notebook中使用%timeit命令来比较运行时间。例如,对于矩阵a和b,在新的笔记本单元中运行命令%timeit a @ b。
PyTorch的分布式数据并行(DistributedDataParallel, DDP)策略。DDP通过将输入数据分割到可用设备上并同时处理这些数据子集来实现并行化。
PyTorch会在每个GPU上启动一个独立的进程,每个进程都会接收并保存一份模型副本,这些副本在训练过程中会进行同步。假设有两个GPU,我们想要用它们来训练一个神经网络,如图A-12所示。

DDP在交互式Python环境(如Jupyter Notebook)中无法正常运行,因为这些环境处理多进程的方式与独立的Python脚本不同。
附录B 参考文献和延伸阅读
FlashAttention是一种高效的自注意力机制实现,通过优化内存访问模式来加速计算过程。FlashAttention在数学上与标准自注意力机制相同,但优化了计算过程以提高效率
高斯误差线性单元(GELU)激活函数结合了经典的ReLU激活函数与正态分布的累积分布函数的特性,能够有效建模层输出,在深度学习模型中实现随机正则化和非线性
下面这篇内容丰富的博客文章指出,当上下文大小小于32 000个词元时,大语言模型中大部分计算是在前馈层而非注意力层中进行的:
- “In the Long (Context) Run”(由Harm de Vries发布)。
以下论文的结果支持这样的观点:大语言模型主要在预训练期间获取事实知识,而微调主要提高其利用这些知识的效率。此外,本研究探讨了使用新事实信息微调大语言模型对其使用已有知识能力的影响,揭示出模型学习新事实的速度较慢,并且在微调过程中引入新知识会增加模型生成错误信息的倾向
附录C 练习的解决方案
复用4.6节的代码,可以计算出当前模型的参数量和显存需求。
当需要对大语言模型输出的多样性和随机性进行调整时,一般要设置采样和温度缩放系数。
附录D 为训练循环添加更多细节和优化功能
学习率预热可以帮助稳定复杂模型(如大语言模型)的训练过程。这个过程可以逐步将学习率从一个非常低的初始值(initial_lr)提升到用户设定的最大值(peak_lr)。在训练开始时使用较小的权重更新,有助于降低模型在训练过程中遭遇大幅度、不稳定更新的风险。
还有一种广泛应用于复杂深度神经网络和大语言模型训练的技术是余弦衰减。这种方法在训练过程中可以调节学习率,使其在预热阶段后呈现余弦曲线的变化。
在其流行的变体中,余弦衰减可以将学习率降低到接近零,模拟半个余弦周期的轨迹。学习率的逐渐降低旨在减缓模型更新权重的速度。这一点特别重要,因为它有助于降低训练过程中超过损失最小值的风险,从而确保后期训练的稳定性。
梯度裁剪也是增强大语言模型训练稳定性的一种重要技术。该方法涉及设定一个阈值,超过该阈值的梯度会被缩放到预定的最大值。这种做法可以确保在反向传播过程中,对模型参数的更新保持在一个可控的范围内。
附录E 使用LoRA进行参数高效微调
LoRA是一种通过仅调整模型权重参数的一小部分,使预训练模型更好地适应特定且通常较小的数据集的技术。
除了可以减少训练期间需要更新的权重数量,将LoRA权重矩阵与原始模型权重分开的能力使LoRA在实践中更加有用。实际上,这一特性允许预训练的模型权重保持不变,并且在使用模型时可以动态地应用LoRA矩阵。
保持LoRA的权重分离在实践中非常有用,因为它使得模型定制变得更加灵活,无须存储多个完整版本的大语言模型。这降低了存储需求并提高了可扩展性,因为在为每个特定客户或应用程序进行定制时,只需调整和保存较小的LoRA矩阵即可。
推理时间扩展
提升大语言模型推理能力的一种方法是推理时间扩展(inference-time scaling)。这个术语在不同背景下可能有不同的含义,但在这里,它指的是增加推理时的计算资源来提高模型输出的质量。
另一种推理时间扩展方法是使用投票和搜索算法。一个简单的例子是多数投票法,即让大语言模型生成多个答案,然后我们通过多数投票来选出最有可能的正确答案。
构建和改进推理模型的3种关键方法。
- 推理时间扩展:一种在不修改或重新训练基础模型的情况下提升推理能力的技术。
- 纯强化学习:DeepSeek-R1-Zero展示了推理可以作为学习行为从无监督微调中涌现。
- 监督微调+强化学习:这是DeepSeek-R1推理模型的开发方法。
DeepSeek的蒸馏方法是通过使用DeepSeek-V3和DeepSeek-R1的中间检查点生成的监督微调数据集,来对较小的大语言模型(如参数量为80亿或700亿的Llama模型以及参数量为5亿~320亿的Qwen 2.5模型)进行指令微调。值得注意的是,这个蒸馏过程中使用的监督微调数据集与训练DeepSeek-R1时使用的数据集完全相同。
构建和优化推理模型的4种策略。
- 推理时间扩展。无须额外训练,但会增加推理成本,随着用户数量或查询量的增加,大规模部署成本会变得更加昂贵。尽管如此,对提升已经很强大的模型的性能来说,它仍然是一种非常直观的方法。
- 纯强化学习。从研究的角度来看,纯强化学习非常有趣,因为它提供了将推理视为一种涌现行为的深刻见解。然而,在实际的模型开发中,强化学习+监督微调是首选方法,因为它能构建更强大的推理模型。
- 强化学习+监督微调。正如前文所述,强化学习+监督微调是构建高性能推理模型的关键方法。
- 蒸馏。蒸馏是一种非常棒的方法,特别适用于创建更小、更高效的模型。然而,蒸馏的局限性在于,它并不能推动创新或生产下一代推理模型。例如,蒸馏总是依赖现有的、更强大的模型来生成监督微调数据。









![图4-6 dim参数在计算张量均值时的示意图。例如,我们有一个维度为[rows, columns]的二维张量(矩阵),使用dim=0将在行方向(垂直,如下图所示)执行操作,结果是对每列的数据进行汇总;使用dim=1或dim=-1将在列方向(水平,如上图所示)执行操作,结果是对每行的数据进行汇总](https://alphahinex.github.io/contents/build-a-large-language-model-from-scratch/4-6.jpeg)


















