用 pytorch 从零开始创建大语言模型(四):从零开始实现一个用于生成文本的GPT模型

news2025/3/20 7:49:28

从零开始创建大语言模型(Python/pytorch )(四):从零开始实现一个用于生成文本的GPT模型

  • 4 从零开始实现一个用于生成文本的GPT模型
    • 4.1 编写 L L M LLM LLM架构
    • 4.2 使用层归一化对激活值进行标准化
    • 4.3 使用GELU激活函数实现前馈神经网络
      • GELU vs. ReLU
      • GELU激活函数的定义
      • 代码实现
    • 4.4 添加捷径连接
    • 4.5 在变换器块中连接注意力机制和线性层
    • 4.6 编写GPT模型
      • GPT-2 模型整体结构
      • 最终的输出层
      • 练习 4.1:计算前馈网络和多头注意力模块的参数数量
      • 练习 4.2:初始化更大规模的 GPT 模型
    • 4.7 生成文本
      • 练习 4.3:使用不同的 dropout 参数
    • 4.8 总结

前面章节内容会慢慢补上!!!

4 从零开始实现一个用于生成文本的GPT模型

本章内容涵盖:

  • 编写一个类似GPT的大型语言模型( L L M LLM LLM),该模型可以被训练以生成类人文本
  • 归一化层激活值以稳定神经网络训练
  • 在深度神经网络中添加捷径连接,以更有效地训练模型
  • 实现Transformer模块以创建不同规模的GPT模型
  • 计算GPT模型的参数数量及存储需求

在上一章中,你学习并实现了多头注意力机制,这是大型语言模型的核心组件之一。在本章中,我们将编写大型语言模型的其他构建模块,并将它们组装成一个类似GPT的模型,随后在下一章中训练该模型以生成类人文本,如图4.1所示。


在这里插入图片描述图4.1 展示了编码大型语言模型( L L M LLM LLM)的三个主要阶段的思维模型,包括在通用文本数据集上对 L L M LLM LLM进行预训练,并在标注数据集上进行微调。本章重点在于实现 L L M LLM LLM的架构,我们将在下一章对其进行训练。


L L M LLM LLM的架构(如图4.1所示)由多个构建模块组成,我们将在本章中逐步实现这些模块。在接下来的部分,我们将首先从整体上概述模型架构,然后再详细介绍各个组件。

from importlib.metadata import version

print("matplotlib version:", version("matplotlib"))
print("torch version:", version("torch"))
print("tiktoken version:", version("tiktoken"))
matplotlib version: 3.10.0
torch version: 2.5.1
tiktoken version: 0.9.0

4.1 编写 L L M LLM LLM架构

大型语言模型( L L M LLM LLM),如 G P T GPT GPT(即生成式预训练变换器),是一种大型深度神经网络架构,旨在逐词(或逐标记)生成新文本。然而,尽管这些模型规模庞大,它们的架构并不像你想象的那么复杂,因为许多组件是重复使用的,我们将在后续内容中看到这一点。图4.2提供了一个类似 G P T GPT GPT L L M LLM LLM的整体视图,并突出显示了其主要组件。


在这里插入图片描述图4.2 展示了 G P T GPT GPT模型的思维模型。除了嵌入层外,它由一个或多个包含掩码多头注意力模块的变换器块组成,而该模块正是我们在上一章中实现的。


如图4.2所示,我们已经涵盖了多个方面,例如输入tokenization和嵌入,以及掩码多头注意力模块。本章的重点将是实现 G P T GPT GPT模型的核心结构,包括其变换器块,我们将在下一章训练该模型以生成类人文本。

在前几章中,为了简化概念和示例,使其能够轻松地适应单页内容,我们使用了较小的嵌入维度。而在本章,我们将模型规模扩展到一个小型 G P T GPT GPT-2模型的级别,具体来说是 R a d f o r d Radford Radford等人在论文《Language Models are Unsupervised Multitask Learners》中描述的最小版本,该模型具有 1.24 1.24 1.24亿个参数。需要注意的是,尽管原始报告中提到 1.17 1.17 1.17亿个参数,但这一数值后来被更正。

第6章将重点介绍如何将预训练权重加载到我们的实现中,并适配到更大规模的 G P T GPT GPT-2模型,包括 3.45 3.45 3.45亿、 7.62 7.62 7.62亿和 15.42 15.42 15.42亿个参数的版本。在深度学习和类似 G P T GPT GPT L L M LLM LLM中,“参数”指的是模型的可训练权重。这些权重本质上是模型的内部变量,在训练过程中不断调整和优化,以最小化特定的损失函数,从而使模型能够从训练数据中学习。

例如,在一个神经网络层中,假设权重由一个 2048 × 2048 2048\times2048 2048×2048维矩阵(或张量)表示,那么该矩阵的每个元素都是一个参数。由于该矩阵有 2048 2048 2048行和 2048 2048 2048列,因此该层的总参数数量为 2048 × 2048 = 4194304 2048\times2048=4194304 2048×2048=4194304个参数。

G P T GPT GPT-2与 G P T GPT GPT-3的对比

需要注意的是,我们专注于 G P T GPT GPT-2,因为 O p e n A I OpenAI OpenAI已公开提供了其预训练模型的权重,我们将在第6章将其加载到我们的实现中。从模型架构的角度来看, G P T GPT GPT-3在本质上与 G P T GPT GPT-2相同,只是将参数规模从 G P T GPT GPT-2的 15 15 15亿扩展到 G P T GPT GPT-3的 1750 1750 1750亿,并且训练数据规模更大。截至撰写本文时, G P T GPT GPT-3的权重尚未公开。

此外, G P T GPT GPT-2也是学习如何实现 L L M LLM LLM的更佳选择,因为它可以在单台笔记本电脑上运行,而 G P T GPT GPT-3的训练和推理则需要 G P U GPU GPU集群。据 L a m b d a L a b s Lambda Labs LambdaLabs估算,使用单块 V 100 V100 V100数据中心 G P U GPU GPU训练 G P T GPT GPT-3需要 355 355 355年,而使用消费级 R T X 8000 RTX8000 RTX8000则需要 665 665 665年。

我们通过以下 P y t h o n Python Python字典来指定小型 G P T GPT GPT-2模型的配置,并将在后续代码示例中使用:

GPT_CONFIG_124M = {
    "vocab_size": 50257, # Vocabulary size
    "context_length": 1024, # Context length
    "emb_dim": 768, # Embedding dimension
    "n_heads": 12, # Number of attention heads
    "n_layers": 12, # Number of layers
    "drop_rate": 0.1, # Dropout rate
    "qkv_bias": False # Query-Key-Value bias
}

GPT_CONFIG_124M字典中,我们使用简洁的变量名,以提高清晰度并避免代码行过长:

  • "vocab_size" 指的是词汇量大小,共 50257 50257 50257个单词,与第2章介绍的 B P E BPE BPE标记器使用的词汇表一致。
  • "context_length" 表示模型可以处理的最大输入标记数,这通过第2章讨论的位置嵌入实现。
  • "emb_dim" 代表嵌入维度,将每个标记转换为 768 768 768维向量。
  • "n_heads" 指的是多头注意力机制中的注意力头数,该机制已在第3章实现。
  • "n_layers" 指定了模型中的变换器块数量,我们将在接下来的章节中详细介绍。
  • "drop_rate" 表示随机失活(dropout)的强度( 0.1 0.1 0.1意味着隐藏单元有 10 % 10\% 10%的概率被丢弃),用于防止过拟合,这在第3章已介绍。
  • "qkv_bias" 决定是否在多头注意力机制中的查询、键和值的线性层中包含偏置向量。最初,我们将禁用该选项,以遵循现代 L L M LLM LLM的标准做法,但在第6章加载来自 O p e n A I OpenAI OpenAI的预训练 G P T GPT GPT-2权重时,我们将重新讨论这一设置。

基于上述配置,我们将在本节实现一个 G P T GPT GPT占位架构(DummyGPTModel),如图4.3所示。这将帮助我们从整体上理解模型的架构,以及在接下来的章节中需要编写哪些组件,以最终组装完整的 G P T GPT GPT模型架构。


在这里插入图片描述图4.3 展示了 G P T GPT GPT架构的编写顺序。在本章中,我们将从 G P T GPT GPT的主干架构(占位架构)开始,然后逐步实现各个核心模块,并最终将它们整合到变换器块中,以构建完整的 G P T GPT GPT模型架构。


图4.3中编号的框表示我们编写最终 G P T GPT GPT架构时需要依次掌握的各个概念的顺序。我们将从第1步开始,实现一个占位的 G P T GPT GPT主干架构,我们称之为DummyGPTModel

代码清单 4.1 占位 G P T GPT GPT模型架构类

import torch
import torch.nn as nn

class DummyGPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        '''创建一个词嵌入层,将输入的 token 索引映射到一个固定维度的向量空间。'''
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        '''创建一个位置嵌入层,将每个位置映射到一个固定维度的向量。'''
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        '''构造一个 Dropout 层,用于在训练时随机丢弃部分神经元,防止过拟合。'''
        self.drop_emb = nn.Dropout(cfg["drop_rate"])
        
        # Use a placeholder for TransformerBlock
        '''
        * 操作符的作用是将列表中的元素拆开,为“解包”操作符。
        将这个列表中的所有元素“解包”,使它们变成 nn.Sequential 的单独参数。
        等效于写成(假设cfg["n_layers"] = 2):
        self.trf_blocks = nn.Sequential(DummyTransformerBlock(cfg), DummyTransformerBlock(cfg))
        创建一个由多个 TransformerBlock 组成的序列,然后用 nn.Sequential 将它们串联在一起。
        '''
        self.trf_blocks = nn.Sequential(*[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])])
        
        # Use a placeholder for LayerNorm
        '''
        创建一个层归一化层,通常用于稳定训练,但这里使用的是一个占位实现(DummyLayerNorm),实际不做任何操作。
        '''
        self.final_norm = DummyLayerNorm(cfg["emb_dim"])
        '''创建一个线性层,将最后得到的嵌入映射到词汇表大小的维度,输出 logits。'''
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

    def forward(self, in_idx):
        """定义模型的前向传播接口,输入为 token 索引组成的张量 in_idx。"""
        batch_size, seq_len = in_idx.shape
        '''
        将 token 索引转换为对应的嵌入向量。
        输出形状为 (batch_size, seq_len, emb_dim),例如 (2, 6, 8)。
        '''
        tok_embeds = self.tok_emb(in_idx)
        '''
        生成位置序列 [0, 1, ..., seq_len-1] 并查找对应的位置信息向量。
        输出形状为 (seq_len, emb_dim),例如 (6, 8)。
        '''
        pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
        '''
        将 token 嵌入与位置嵌入相加,融合语义信息和位置信息。
        pos_embeds 的形状 (6, 8) 自动扩展到 (2, 6, 8) 与 tok_embeds 相加。
        1. tok_embeds[0](6,8)+pos_embeds(6, 8) ->[0](6,8)
        2. tok_embeds[1](6,8)+pos_embeds(6, 8) ->[1](6,8)
        变成(2, 6, 8)
        '''
        x = tok_embeds + pos_embeds
        x = self.drop_emb(x)
        '''
        将 x 依次传入所有 DummyTransformerBlock 层处理。
        '''
        x = self.trf_blocks(x)
        '''
        对 x 进行层归一化操作,稳定训练。
        '''
        x = self.final_norm(x)
        '''
        通过线性变换,将每个 token 的 8 维向量映射到词汇表大小维度(例如 100),得到每个词的 logits。
        '''
        logits = self.out_head(x)
        return logits


class DummyTransformerBlock(nn.Module):
    """定义一个占位用的 TransformerBlock 模块。"""
    def __init__(self, cfg):
        super().__init__()
        # A simple placeholder

    def forward(self, x):
        # This block does nothing and just returns its input.
        return x


class DummyLayerNorm(nn.Module):
    """定义一个占位用的 LayerNorm 层,用于模拟接口。"""
    def __init__(self, normalized_shape, eps=1e-5):
        super().__init__()
        # The parameters here are just to mimic the LayerNorm interface.

    def forward(self, x):
        # This layer does nothing and just returns its input.
        return x

pytorch小记(十四):pytorch中 nn.Embedding 详解

DummyGPTModel类在这段代码中定义了一个使用 P y T o r c h PyTorch PyTorch神经网络模块(nn.Module)的简化版 G P T GPT GPT模型。DummyGPTModel类的模型架构包括标记嵌入和位置嵌入、随机失活、若干个变换器块(DummyTransformerBlock)、最终的层归一化(DummyLayerNorm),以及一个线性输出层(out_head)。模型的配置通过 P y t h o n Python Python字典传入,例如我们之前创建的GPT_CONFIG_124M字典。

forward方法描述了数据在模型中的流动过程:它计算输入索引的标记嵌入和位置嵌入,应用随机失活,然后将数据传递至变换器块进行处理,接着应用归一化,并最终通过线性输出层生成对数概率(logits)。

上述代码已经是一个可运行的模型,我们将在本节后续部分准备输入数据并进行测试。然而,目前需要注意的是,代码中我们使用了占位符(DummyLayerNormDummyTransformerBlock)来代替变换器块和层归一化,这些组件将在后续部分实现。

接下来,我们将准备输入数据并初始化一个新的 G P T GPT GPT模型,以演示其使用方式。基于我们在第2章中编写标记器的内容,图4.4提供了数据在 G P T GPT GPT模型中流入和流出的整体概览。


在这里插入图片描述
图4.4 展示了数据流入 G P T GPT GPT模型的全局视图,包括输入数据的标记化、嵌入处理,并最终输入到 G P T GPT GPT模型。需要注意,在我们之前编写的DummyGPTClass中,标记嵌入是在 G P T GPT GPT模型内部处理的。在 L L M LLM LLM中,嵌入输入标记的维度通常与输出维度相匹配。这里的输出嵌入代表了我们在第3章讨论的上下文向量。


为了实现图4.4所示的步骤,我们使用第2章介绍的tiktoken标记器对一批包含两条文本输入的数据进行标记化,以供 G P T GPT GPT模型使用。

import tiktoken

tokenizer = tiktoken.get_encoding("gpt2")

batch = []

txt1 = "Every effort moves you"
txt2 = "Every day holds a"

batch.append(torch.tensor(tokenizer.encode(txt1)))
batch.append(torch.tensor(tokenizer.encode(txt2)))
batch = torch.stack(batch, dim=0)
print(batch)

由此得到的两个文本的标记 ID 如下:

tensor([[6109, 3626, 6100,  345], #A
        [6109, 1110, 6622,  257]])

接下来,我们初始化一个具有 1.24 1.24 1.24亿参数的DummyGPTModel实例,并将标记化后的数据批次输入到模型中:

torch.manual_seed(123)
model = DummyGPTModel(GPT_CONFIG_124M)
logits = model(batch)
print("Output shape:", logits.shape)
print(logits)

模型的输出通常称为对数概率(logits),具体如下:

Output shape: torch.Size([2, 4, 50257])
tensor([[[-0.9289,  0.2748, -0.7557,  ..., -1.6070,  0.2702, -0.5888],
         [-0.4476,  0.1726,  0.5354,  ..., -0.3932,  1.5285,  0.8557],
         [ 0.5680,  1.6053, -0.2155,  ...,  1.1624,  0.1380,  0.7425],
         [ 0.0447,  2.4787, -0.8843,  ...,  1.3219, -0.0864, -0.5856]],

        [[-1.5474, -0.0542, -1.0571,  ..., -1.8061, -0.4494, -0.6747],
         [-0.8422,  0.8243, -0.1098,  ..., -0.1434,  0.2079,  1.2046],
         [ 0.1355,  1.1858, -0.1453,  ...,  0.0869, -0.1590,  0.1552],
         [ 0.1666, -0.8138,  0.2307,  ...,  2.5035, -0.3055, -0.3083]]],
       grad_fn=<UnsafeViewBackward0>)

Output shape: torch.Size([2, 4, 50257]):可以理解成2个batch,每个batch有4个单词(token),每个单词(token)用50257维来表示

输出张量包含两行,对应于两条文本样本。每个文本样本由 4 4 4个标记组成,每个标记是一个 50257 50257 50257维的向量,这与标记器的词汇表大小相匹配。

嵌入层的维度为 50257 50257 50257,因为这些维度中的每一个都对应于词汇表中的一个唯一标记。在本章末尾,当我们实现后处理代码时,我们将把这些 50257 50257 50257维的向量转换回标记 I D ID ID,然后解码为单词。

现在,我们已经从整体上了解了 G P T GPT GPT架构及其输入和输出,接下来将在后续部分逐步编写具体的占位组件,从实现真实的层归一化类开始,以替换之前代码中的DummyLayerNorm

4.2 使用层归一化对激活值进行标准化

在训练具有多个层的深度神经网络时,可能会遇到梯度消失或梯度爆炸等问题。这些问题会导致训练过程不稳定,使得网络难以有效地调整其权重。这意味着学习过程难以找到一组能够最小化损失函数的参数(权重)。换句话说,网络难以学习数据中的潜在模式,从而无法做出准确的预测或决策。

(如果你对神经网络训练和梯度的概念不熟悉,可以参考附录A中的A.4节 《自动微分简明介绍》。然而,理解本教程内容并不需要对梯度有深奥的数学理解。)

在本节中,我们将实现层归一化,以提高神经网络训练的稳定性和效率。

层归一化的核心思想是调整神经网络层的激活值(输出),使其均值为 0 0 0,方差为 1 1 1,也称为单位方差。这种调整可以加速收敛,使网络更快地找到有效的权重,并确保训练过程的稳定性和可靠性。正如我们在上一节基于DummyLayerNorm占位符所看到的,在 G P T GPT GPT-2及现代变换器架构中,层归一化通常应用于多头注意力模块的前后,以及最终输出层的前面。

在我们编写层归一化的代码之前,图4.5提供了层归一化的可视化概览。


在这里插入图片描述图4.5展示了如何对 5 5 5个层的输出(即激活值)进行归一化,使其均值为 0 0 0,方差为 1 1 1


我们可以通过以下代码重新创建图4.5所示的示例,其中我们实现了一个具有 5 5 5个输入和 6 6 6个输出的神经网络层,并将其应用于两个输入示例:

torch.manual_seed(123)

# create 2 training examples with 5 dimensions (features) each
batch_example = torch.randn(2, 5) 
print(batch_example)
layer = nn.Sequential(nn.Linear(5, 6), nn.ReLU())
out = layer(batch_example)
print(out)

这将打印出以下张量,其中第一行列出了第一个输入的层输出,第二行列出了第二个输入的层输出:

tensor([[-0.1115,  0.1204, -0.3696, -0.2404, -1.1969],
        [ 0.2093, -0.9724, -0.7550,  0.3239, -0.1085]])
tensor([[0.2260, 0.3470, 0.0000, 0.2216, 0.0000, 0.0000],
        [0.2133, 0.2394, 0.0000, 0.5198, 0.3297, 0.0000]],
       grad_fn=<ReluBackward0>)

我们编写的神经网络层由一个线性层(Linear)和一个非线性激活函数 R e L U ReLU ReLU(即修正线性单元)组成, R e L U ReLU ReLU是神经网络中的标准激活函数。如果你对 R e L U ReLU ReLU不熟悉,它的作用是将负输入截断为 0 0 0,确保层的输出仅包含正值,这也解释了为什么最终的层输出中不包含任何负值。(需要注意的是,在 G P T GPT GPT模型中,我们将使用另一种更复杂的激活函数,我们将在下一节介绍。)

在对这些输出应用层归一化之前,让我们先检查其均值和方差:

mean = out.mean(dim=-1, keepdim=True)
var = out.var(dim=-1, keepdim=True)

print("Mean:\n", mean)
print("Variance:\n", var)

输出结果如下:

Mean:
 tensor([[0.1324],
        [0.2170]], grad_fn=<MeanBackward1>)
Variance:
 tensor([[0.0231],
        [0.0398]], grad_fn=<VarBackward0>)

上方均值张量的第一行包含第一个输入行的均值,第二行包含第二个输入行的均值。

在计算均值或方差等操作时,使用keepdim=True可以确保输出张量保持与输入张量相同的形状,即使操作沿着由dim参数指定的维度进行降维。例如,如果不使用keepdim=True,返回的均值张量将是一个 2 2 2维向量[0.1324, 0.2170],而不是一个 2 × 1 2\times1 2×1维矩阵[[0.1324], [0.2170]]

dim参数指定在张量的哪个维度上计算统计量(如均值或方差),其作用如图4.6所示。


在这里插入图片描述图4.6 展示了在计算张量均值时dim参数的作用。例如,如果我们有一个 2 D 2D 2D张量(矩阵),其维度为 [ 行 , 列 ] [行,列] [,],那么使用dim=0将在行方向(纵向,见图底部)执行操作,得到的输出聚合了每列的数据。而使用dim=1dim=-1将在列方向(横向,见图顶部)执行操作,得到的输出聚合了每行的数据。


如图4.6所示,对于 2 D 2D 2D张量(如矩阵),在计算均值或方差等操作时,使用dim=-1等同于使用dim=1。这是因为-1表示张量的最后一个维度,在 2 D 2D 2D张量中对应于列。

在后续为 G P T GPT GPT模型添加层归一化时,该模型会生成形状为[batch_size, num_tokens, embedding_size] 3 D 3D 3D张量,我们仍然可以使用dim=-1在最后一个维度上进行归一化,而无需将dim=1改为dim=2

接下来,让我们对之前获得的层输出应用层归一化。该操作包括减去均值并除以方差的平方根(即标准差):

out_norm = (out - mean) / torch.sqrt(var)
mean = out_norm.mean(dim=-1, keepdim=True)
var = out_norm.var(dim=-1, keepdim=True)

print("Normalized layer outputs:\n", out_norm)
print("Mean:\n", mean)
print("Variance:\n", var)

从结果可以看出,归一化后的层输出现在包含了负值,并且均值为 0 0 0,方差为 1 1 1

Normalized layer outputs:
 tensor([[ 0.6159,  1.4126, -0.8719,  0.5872, -0.8719, -0.8719],
        [-0.0189,  0.1121, -1.0876,  1.5173,  0.5647, -1.0876]],
       grad_fn=<DivBackward0>)
Mean:
 tensor([[9.9341e-09],
        [0.0000e+00]], grad_fn=<MeanBackward1>)
Variance:
 tensor([[1.0000],
        [1.0000]], grad_fn=<VarBackward0>)

需要注意的是,输出张量中的值9.9341e-08是科学计数法的表示方式,相当于 9.9341 × 1 0 − 9 9.9341\times10^{-9} 9.9341×109,即十进制形式的0.0000000099341。这个值非常接近 0 0 0,但由于计算机表示数值时的有限精度,可能会累积微小的数值误差,因此不会完全等于 0 0 0

为了提高可读性,我们可以关闭科学计数法的显示方式,只需将sci_mode设置为False

torch.set_printoptions(sci_mode=False)
print("Mean:\n", mean)
print("Variance:\n", var)

输出结果:

Mean:
 tensor([[    0.0000],
        [    0.0000]], grad_fn=<MeanBackward1>)
Variance:
 tensor([[1.0000],
        [1.0000]], grad_fn=<VarBackward0>)

到目前为止,在本节中,我们已经逐步实现并应用了层归一化。现在,我们将这一过程封装到一个 P y T o r c h PyTorch PyTorch模块中,以便在后续的 G P T GPT GPT模型中使用:

代码清单 4.2 层归一化类

class LayerNorm(nn.Module):
    def __init__(self, emb_dim):
        super().__init__()
        self.eps = 1e-5
        self.scale = nn.Parameter(torch.ones(emb_dim))
        self.shift = nn.Parameter(torch.zeros(emb_dim))

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        norm_x = (x - mean) / torch.sqrt(var + self.eps)
        return self.scale * norm_x + self.shift

这一层归一化的实现针对输入张量x的最后一个维度进行操作,该维度对应于嵌入维度(emb_dim)。变量eps是一个很小的常数( ϵ = 1 0 − 5 \epsilon=10^{-5} ϵ=105),用于防止归一化过程中出现除零错误。

scaleshift是两个可训练参数,它们的维度与输入相同。在 L L M LLM LLM的训练过程中,模型会自动调整这些参数,以优化其在训练任务上的表现。这使得模型能够学习适合其处理数据的最佳缩放和偏移,从而提高训练效果。

有偏方差计算

在方差计算方法中,我们选择了一个实现细节,即将unbiased=False。对于好奇这一点的读者,在方差计算中,我们使用公式中的样本数量 n n n作为分母进行计算,而不是使用 n − 1 n-1 n1(即贝塞尔校正)。贝塞尔校正通常用于修正样本方差估计中的偏差,而我们的做法会导致所谓的有偏方差估计

对于大型语言模型( L L M LLM LLM),由于嵌入维度 n n n通常非常大,使用 n n n n − 1 n-1 n1之间的差异实际上可以忽略不计。我们选择这种方式,以确保与 G P T GPT GPT-2模型的归一化层保持兼容,并且这样做也符合 T e n s o r F l o w TensorFlow TensorFlow的默认行为( G P T GPT GPT-2的原始实现基于 T e n s o r F l o w TensorFlow TensorFlow)。采用类似的设置可以确保我们的方法与第6章将要加载的预训练权重兼容。

现在,让我们在实践中测试LayerNorm模块,并将其应用到批量输入数据上:

ln = LayerNorm(emb_dim=5)
out_ln = ln(batch_example)

mean = out_ln.mean(dim=-1, keepdim=True)
var = out_ln.var(dim=-1, unbiased=False, keepdim=True)

print("Mean:\n", mean)
print("Variance:\n", var)

从结果可以看出,层归一化代码正常工作,并且归一化后的两个输入数据的均值为 0 0 0,方差为 1 1 1

Mean:
 tensor([[    -0.0000],
        [     0.0000]], grad_fn=<MeanBackward1>)
Variance:
 tensor([[1.0000],
        [1.0000]], grad_fn=<VarBackward0>)

在本节中,我们介绍了 G P T GPT GPT架构所需的一个重要构建模块,如图4.7所示。


在这里插入图片描述图4.7 展示了本章中实现 G P T GPT GPT架构所需的各个构建模块的思维模型。


在下一节中,我们将介绍 GELU(高斯误差线性单元)激活函数,它是 L L M LLM LLM中常用的激活函数之一,而不是本节中使用的传统 R e L U ReLU ReLU函数。

层归一化 vs. 批量归一化

如果你熟悉 批量归一化(Batch Normalization),这是一种常见的神经网络归一化方法,那么你可能会好奇它与 层归一化(Layer Normalization) 的对比。

与批量归一化不同,批量归一化是在 批量(batch) 维度 上进行归一化,而层归一化则是在 特征(feature) 维度 上进行归一化。

由于 L L M LLM LLM通常需要大量计算资源,可用的硬件或特定的使用场景可能会影响训练或推理时的批量大小(batch size)。而层归一化对每个输入独立进行归一化,不受批量大小的影响,因此在这些情况下提供了更大的灵活性和稳定性。

这种特性在 分布式训练计算资源受限的环境 中尤其有利,使得模型能够在不同的设备和计算环境下保持稳定的表现。

4.3 使用GELU激活函数实现前馈神经网络

在本节中,我们将实现一个小型神经网络子模块,该模块是 L L M LLM LLM中变换器块的一部分。我们首先实现 GELU(高斯误差线性单元)激活函数,它在此神经网络子模块中起着关键作用。(如果需要额外了解如何在 P y T o r c h PyTorch PyTorch中实现神经网络,请参考附录A的A.5节 《实现多层神经网络》。)

GELU vs. ReLU

历史上,ReLU(修正线性单元)因其简单性和在各种神经网络架构中的有效性而被广泛使用。然而,在 L L M LLM LLM中,除了传统的 R e L U ReLU ReLU之外,还使用了其他几种激活函数,其中两个重要的例子是:

  • GELU(高斯误差线性单元)
  • SwiGLU(Sigmoid加权线性单元)

GELUSwiGLU R e L U ReLU ReLU更复杂,它们分别结合了 高斯分布Sigmoid门控线性单元,能够为深度学习模型提供更好的性能。

GELU激活函数的定义

GELU激活函数可以用多种方式实现,最精确的数学定义如下:

GELU(x) = x ⋅ Φ ( x ) \text{GELU(x)} = \text{x} \cdot \Phi(\text{x}) GELU(x)=xΦ(x)

其中, Φ ( x ) \Phi(x) Φ(x)标准高斯分布的累积分布函数(CDF)

但在实际应用中,通常采用计算量更小的近似版本( G P T GPT GPT-2模型的原始训练也是基于此近似实现):

GELU(x) ≈ 0.5 ⋅ x ⋅ ( 1 + tanh ⁡ ( 2 / π ⋅ ( x + 0.044715 ⋅ x 3 ) ) ) \text{GELU(x)} \approx 0.5 \cdot \text{x} \cdot (1 + \tanh(\sqrt{2/\pi} \cdot (\text{x} + 0.044715 \cdot \text{x}^3))) GELU(x)0.5x(1+tanh(2/π (x+0.044715x3)))

代码实现

我们可以将此函数实现为 P y T o r c h PyTorch PyTorch模块,如下所示:

代码清单 4.3 GELU激活函数的实现

class GELU(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        return 0.5 * x * (1 + torch.tanh(
            torch.sqrt(torch.tensor(2.0 / torch.pi)) * (x + 0.044715 * torch.pow(x, 3))
        ))

接下来,为了直观地了解 GELU 函数的形状,并与 ReLU 函数进行对比,我们可以将这两个函数绘制在同一张图上:

import matplotlib.pyplot as plt

gelu, relu = GELU(), nn.ReLU()
x = torch.linspace(-3, 3, 100)  # A
y_gelu, y_relu = gelu(x), relu(x)

plt.figure(figsize=(8, 3))
for i, (y, label) in enumerate(zip([y_gelu, y_relu], ["GELU", "ReLU"]), 1):
    plt.subplot(1, 2, i)
    plt.plot(x, y)
    plt.title(f"{label} activation function")
    plt.xlabel("x")
    plt.ylabel(f"{label}(x)")
    plt.grid(True)

plt.tight_layout()
plt.show()

图4.8 所示的绘图结果可以看出,ReLU 是一个分段线性函数:当输入为正时,它直接输出输入值;当输入为负时,它输出 0 0 0

GELU 是一个平滑的非线性函数,它近似 R e L U ReLU ReLU,但对于负值输入仍然保持了非零梯度。


在这里插入图片描述图4.8 展示了使用matplotlib绘制的 GELUReLU 函数的输出。横轴( x x x轴)表示函数输入,纵轴( y y y轴)表示函数输出。


GELU 的平滑特性(如图4.8所示)可以在训练过程中带来更好的优化性能,因为它允许对模型参数进行更细微的调整。相比之下,ReLU 0 0 0处存在一个尖角,这可能会在优化过程中带来困难,尤其是在深层网络复杂架构中。

此外,与 ReLU 不同,ReLU 对任何负输入都会输出 0 0 0,而 GELU 对负输入仍然允许一个小的非零输出。这意味着在训练过程中,即使某些神经元接收到负输入,它们仍然可以在一定程度上参与学习过程,尽管其贡献比正输入要小。

接下来,我们使用 GELU 函数来实现一个小型神经网络模块 FeedForward,该模块将在后续的 L L M LLM LLM变换器块中使用:

代码清单 4.4 前馈神经网络模块

class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
            GELU(),
            nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
        )

    def forward(self, x):
        return self.layers(x)

print(GPT_CONFIG_124M["emb_dim"])
768

如上面的代码所示,FeedForward 模块是一个由 两个线性层(Linear layers)GELU激活函数 组成的小型神经网络。在 具有1.24亿参数的 G P T GPT GPT模型 中,该模块接收 输入批次(batches),其中每个标记(token)的嵌入维度(embedding size)768,这一配置由GPT_CONFIG_124M字典中的参数 GPT_CONFIG_124M["emb_dim"] = 768 确定。

图4.9 展示了当输入通过该 前馈神经网络(Feed Forward Neural Network) 时,嵌入维度如何在该小型网络内部进行变换。


在这里插入图片描述图4.9 直观地展示了前馈神经网络各层之间的连接关系。值得注意的是,该神经网络可以处理不同的批量大小(batch size)输入标记数(num_tokens),但每个标记的嵌入维度(embedding size) 在初始化权重时即被确定,并保持固定


按照 图4.9 中的示例,我们初始化一个新的 FeedForward 模块,其 标记嵌入维度(embedding size)768,并向其输入一个包含 2个样本,每个样本包含3个标记 的批量输入:

ffn = FeedForward(GPT_CONFIG_124M)
x = torch.rand(2, 3, 768)  # A
out = ffn(x)
print(out.shape)

如我们所见,输出张量的形状与输入张量相同

torch.Size([2, 3, 768])

在本节中实现的 FeedForward 模块在 增强模型的学习能力和泛化能力 方面发挥着重要作用。

尽管该模块的 输入和输出维度相同,但其 内部 通过第一层线性变换(Linear Layer) 将嵌入维度扩展到 更高维度空间,然后经过 GELU非线性激活,最终通过 第二层线性变换 将维度 收缩回原始尺寸,如 图4.10 所示。

这种设计使得模型能够探索更丰富的表示空间,进而提升对数据的理解能力。


在这里插入图片描述图4.10 展示了 前馈神经网络(Feed Forward Neural Network)层输出的扩展与收缩过程:1. 第一层线性变换:将输入 扩展4倍,即 从768维度扩展到3072维度。2. GELU非线性激活:对扩展后的向量进行非线性变换。3. 第二层线性变换:将3072维度压缩回768维度,以匹配原始输入的维度。


此外,输入和输出维度的一致性 简化了模型架构,使得我们可以 堆叠多个层(我们将在后续实现),而无需在层之间调整维度,从而提高模型的 可扩展性(scalability)

图4.11 所示,我们已经实现了 大部分 L L M LLM LLM的核心构建模块


在这里插入图片描述图4.11 展示了本章涵盖的主题,其中 黑色对勾标记表示我们已经完成的部分


在下一节中,我们将介绍 捷径连接(shortcut connections) 的概念。这些连接被插入到神经网络的不同层之间,对于 提升深度神经网络架构的训练性能 至关重要。

4.4 添加捷径连接

接下来,我们讨论 捷径连接(shortcut connections) 的概念,这种连接也称为 跳跃连接(skip connections)残差连接(residual connections)

最初,捷径连接 是为 计算机视觉中的深度网络(尤其是残差网络(ResNet))提出的,目的是 缓解梯度消失问题(vanishing gradient problem)

梯度消失问题 指的是,在训练过程中,梯度(用于指导权重更新)在向后传播时逐渐变小,导致模型难以有效训练 靠近输入端的早期层,如 图4.12 所示。


在这里插入图片描述图4.12 比较了 一个包含5层的深度神经网络,其中:

  • 左侧没有捷径连接 的网络。
  • 右侧添加了捷径连接 的网络。

捷径连接的核心思想是:将某一层的输入直接加到该层的输出上,从而 创建一个绕过某些层的额外路径图1.1 中所示的 梯度(gradient) 代表 每一层的平均绝对梯度值,我们将在后续的代码示例中计算这些梯度。


图4.12 所示,捷径连接(shortcut connection) 创建了一条 额外的、较短的梯度传播路径,使梯度可以绕过一层或多层直接流经网络。这种 跳跃连接(skip connection) 通过 将前一层的输出加到后续层的输出上 实现,因此在训练的 反向传播(backward pass) 过程中,它们在 保持梯度流动 方面起到了至关重要的作用。

在下面的代码示例中,我们实现了 图4.12 中所示的神经网络,并探索如何在 前向传播(forward method) 中添加 捷径连接

代码清单 4.5 实现带有捷径连接的深度神经网络

class ExampleDeepNeuralNetwork(nn.Module):
    def __init__(self, layer_sizes, use_shortcut):
        super().__init__()
        self.use_shortcut = use_shortcut
        self.layers = nn.ModuleList([
            # 5 层,每层包含一个线性层(Linear)和GELU激活函数
            nn.Sequential(nn.Linear(layer_sizes[0], layer_sizes[1]), GELU()),
            nn.Sequential(nn.Linear(layer_sizes[1], layer_sizes[2]), GELU()),
            nn.Sequential(nn.Linear(layer_sizes[2], layer_sizes[3]), GELU()),
            nn.Sequential(nn.Linear(layer_sizes[3], layer_sizes[4]), GELU()),
            nn.Sequential(nn.Linear(layer_sizes[4], layer_sizes[5]), GELU())
        ])

    def forward(self, x):
        for layer in self.layers:
            # 计算当前层的输出
            layer_output = layer(x)
            # 如果启用了捷径连接,并且形状匹配,则应用捷径连接
            if self.use_shortcut and x.shape == layer_output.shape:
                x = x + layer_output
            else:
                x = layer_output
        return x

代码解析

  • 该代码实现了一个 深度神经网络,包含 5层,每一层由:
    • 一个线性层(Linear layer)
    • GELU激活函数
  • 前向传播(forward pass) 过程中:
    1. 逐层计算输入的输出
    2. 如果 self.use_shortcut=True 且当前层的输入与输出形状相同,则 应用捷径连接(x = x + layer_output)
    3. 否则,直接使用当前层的输出(x = layer_output)

初始化一个不带捷径连接的神经网络

我们首先初始化 一个没有捷径连接的神经网络。这里,每一层都被初始化为:

  • 输入为3个数值
  • 输出为3个数值
  • 最后一层输出1个数值
layer_sizes = [3, 3, 3, 3, 3, 1]
sample_input = torch.tensor([[1., 0., -1.]])

torch.manual_seed(123)  # 指定随机种子,以确保可复现性
model_without_shortcut = ExampleDeepNeuralNetwork(
    layer_sizes, 
    use_shortcut=False
)

这样,我们创建了一个 不包含捷径连接的神经网络,其权重由 随机种子 设定,以确保实验结果可复现。

接下来,我们实现一个 计算模型反向传播梯度 的函数:

def print_gradients(model, x):
    # 前向传播
    output = model(x)
    target = torch.tensor([[0.]])
    
    # 计算损失,衡量模型输出与目标值(此处设为0)的接近程度
    loss = nn.MSELoss()
    loss = loss(output, target)

    # 反向传播计算梯度
    loss.backward()

    for name, param in model.named_parameters():
        if 'weight' in name:
            # 打印权重梯度的平均绝对值
            print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")

在上述代码中,我们定义了损失函数,用于计算模型输出与用户指定的目标值(此处设为 0)的误差。然后,调用 loss.backward(),让 PyTorch 自动计算模型每一层的梯度。我们可以通过 model.named_parameters() 迭代模型的权重参数 并获取其梯度信息。假设某一层的权重参数矩阵是 3 × 3 3\times3 3×3,那么该层将会有 3 × 3 3\times3 3×3 个梯度值。为了更方便地比较不同层的梯度,我们 计算并打印这些梯度的平均绝对值

关于 .backward()

  • .backward() 方法PyTorch 中的一个便捷函数,可 自动计算损失函数的梯度,无需手动实现梯度计算的数学公式。
  • 这一方法使得 深度神经网络的训练更加高效和便捷

如果你对 梯度(gradients)神经网络训练过程 还不熟悉,建议阅读 附录A 中的以下内容:

  • A.4: 《自动微分简明介绍(Auto Differentiation Made Easy)》
  • A.7: 《典型训练循环(A Typical Training Loop)》

现在,我们使用 print_gradients 函数,并将其应用于 不带跳跃连接(skip connections) 的模型:

print_gradients(model_without_shortcut, sample_input)

输出如下:

layers.0.0.weight has gradient mean of 0.00020173587836325169
layers.1.0.weight has gradient mean of 0.0001201116101583466
layers.2.0.weight has gradient mean of 0.0007152041653171182
layers.3.0.weight has gradient mean of 0.001398873864673078
layers.4.0.weight has gradient mean of 0.005049646366387606

print_gradients 函数的输出可以看出,梯度值从最后一层(layers.4) 到第一层(layers.0) 逐渐减小,这种现象被称为 梯度消失问题(vanishing gradient problem)

创建带跳跃连接的模型,并进行比较

接下来,我们 实例化一个包含跳跃连接(skip connections) 的模型,并观察梯度分布的变化:

torch.manual_seed(123)
model_with_shortcut = ExampleDeepNeuralNetwork(
    layer_sizes, 
    use_shortcut=True
)
print_gradients(model_with_shortcut, sample_input)

输出如下:

layers.0.0.weight has gradient mean of 0.22169792652130127
layers.1.0.weight has gradient mean of 0.20694106817245483
layers.2.0.weight has gradient mean of 0.32896995544433594
layers.3.0.weight has gradient mean of 0.2665732502937317
layers.4.0.weight has gradient mean of 1.3258541822433472

带跳跃连接的模型 的梯度分布可以看出:

  • 梯度不再随着层数增加而逐渐减小,而是 在各层保持相对较大
  • 这证明了 跳跃连接有效缓解了梯度消失问题,使得梯度能够更顺畅地从 输出层 传递到 输入层,从而改善深度神经网络的训练性能

这里 “从 输出层 传递到 输入层” 指的是反向传播过程!

从输出结果可以看出,最后一层(layers.4) 的梯度仍然较大,但梯度值在向第一层(layers.0) 传播的过程中趋于稳定,并且不会缩小到极小的值

捷径连接(shortcut connections) 在深度神经网络中对于克服 梯度消失问题(vanishing gradient problem) 的限制至关重要。捷径连接是 超大规模模型(如 L L M LLM LLM) 的核心构建模块,它们在训练 G P T GPT GPT模型时确保梯度能够稳定地 在各层之间流动,从而 提高训练的有效性

在介绍完 捷径连接 之后,我们将在 下一节整合 之前讨论的所有核心概念:

  • 层归一化(Layer Normalization)
  • GELU激活函数
  • 前馈模块(Feed Forward Module)
  • 捷径连接(Shortcut Connections)

最终,我们将 在变换器块(Transformer Block) 中实现这些组件,这是 G P T GPT GPT架构的最后一个构建模块

4.5 在变换器块中连接注意力机制和线性层

在本节中,我们将实现 变换器块(Transformer Block),这是 GPT 及其他 L L M LLM LLM架构核心构建模块

G P T GPT GPT-2(1.24亿参数) 的架构 中,该变换器块被 重复多次,并整合了我们之前介绍的多个概念:

  • 多头注意力机制(Multi-Head Attention)
  • 层归一化(Layer Normalization)
  • 随机失活(Dropout)
  • 前馈层(Feed Forward Layers)
  • GELU激活函数

这一结构如 图4.13 所示。

在下一节中,我们将 把该变换器块与 G P T GPT GPT架构的其余部分连接,完成最终的 完整 G P T GPT GPT模型


在这里插入图片描述图4.13 展示了 变换器块(Transformer Block) 的组成结构:

  • 底部 显示了 输入标记(tokens),它们已经被 转换为768维的向量(即 嵌入表示)。
  • 每一行(row) 对应 一个标记的向量表示
  • 变换器块的输出输入的维度相同,因此可以进一步输入到 后续的 L L M LLM LLM 进行处理。

图4.13 所示,变换器块(Transformer Block) 结合了多个关键组件,包括:

  • 第3章介绍的掩码多头注意力模块(Masked Multi-Head Attention Module)
  • 第4.3节实现的前馈模块(FeedForward Module)

当变换器块处理 输入序列 时,序列中的每个元素(如单词或子词标记) 都会被表示为一个 固定大小的向量(在 图4.13 中,该向量维度为 768)。变换器块的操作(包括 多头注意力机制和前馈层不会改变输入向量的维度,而是通过 变换这些向量的表示方式 来增强模型的处理能力。

核心思想

  • 多头自注意力机制(Multi-Head Self-Attention) 负责 识别和分析输入序列中元素之间的关系
  • 前馈神经网络(Feed Forward Network, FFN)每个位置单独修改数据

这种 组合方式 既能 让模型更深入地理解输入序列,又能 增强模型处理复杂数据模式的能力

代码实现

我们可以使用以下代码实现 TransformerBlock

代码清单 4.6 G P T GPT GPT的变换器块组件

from previous_chapters import MultiHeadAttention

class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.att = MultiHeadAttention(
            d_in=cfg["emb_dim"],
            d_out=cfg["emb_dim"],
            context_length=cfg["context_length"],
            num_heads=cfg["n_heads"], 
            dropout=cfg["drop_rate"],
            qkv_bias=cfg["qkv_bias"]
        )
        '''
        (1,4)@(4,16) -> (1,16)
        (1,16)@(16,4) -> (1,4)
        '''
        self.ff = FeedForward(cfg)
        self.norm1 = LayerNorm(cfg["emb_dim"])
        self.norm2 = LayerNorm(cfg["emb_dim"])
        self.drop_shortcut = nn.Dropout(cfg["drop_rate"])

    def forward(self, x):
        # Shortcut connection for attention block
        shortcut = x
        x = self.norm1(x)
        x = self.att(x)  # Shape [batch_size, num_tokens, emb_size]
        x = self.drop_shortcut(x)
        x = x + shortcut  # Add the original input back

        # Shortcut connection for feed forward block
        shortcut = x
        x = self.norm2(x)
        x = self.ff(x)
        x = self.drop_shortcut(x)
        x = x + shortcut  # Add the original input back

        return x

下载 previous_chapters.py 到相同目录中

按照图4.13流程来走,很容易理解!

该代码定义了 TransformerBlock 类,这是一个 P y T o r c h PyTorch PyTorch 实现的变换器块,其中包含:

  • 多头注意力机制(MultiHeadAttention)
  • 前馈网络(FeedForward)
  • 层归一化(LayerNorm)
  • 随机失活(Dropout)

这些组件的配置基于 提供的配置字典(cfg),例如 GPT_CONFIG_124M

Pre-LayerNorm vs. Post-LayerNorm

  • 本实现 中,层归一化 (LayerNorm) 应用在多头注意力和前馈网络之前,这种方式被称为 “Pre-LayerNorm”
  • 早期的变换器架构(如原始 T r a n s f o r m e r Transformer Transformer模型) 中,层归一化是应用在这些模块之后,这种方式被称为 “Post-LayerNorm”
  • “Post-LayerNorm” 可能会导致 训练动态较差,因此现代 L L M LLM LLM通常采用 “Pre-LayerNorm”

前向传播 (Forward Pass) 该类还实现了 前向传播,其中:

  • 每个组件 后面都加上捷径连接(shortcut connection),即 将块的输入添加到其输出
  • 这种 捷径连接 能够 帮助梯度在训练过程中更顺畅地传播,从而 改进深度模型的学习能力(详见 4.4节)。

代码示例:实例化变换器块
使用 GPT_CONFIG_124M 配置,我们创建一个 变换器块 并向其输入一些样本数据:

torch.manual_seed(123)
x = torch.rand(2, 4, 768)  # A
block = TransformerBlock(GPT_CONFIG_124M)
output = block(x)

print("Input shape:", x.shape)
print("Output shape:", output.shape)

输出:

Input shape: torch.Size([2, 4, 768])
Output shape: torch.Size([2, 4, 768])

保持输入输出形状不变
从代码输出可以看出:

  • 变换器块的输入和输出维度相同,即:
    Input shape = Output shape = ( b a t c h _ s i z e , s e q u e n c e _ l e n g t h , e m b e d d i n g _ s i z e ) \text{Input shape} = \text{Output shape} = (batch\_size, sequence\_length, embedding\_size) Input shape=Output shape=(batch_size,sequence_length,embedding_size)
  • 这表明 变换器架构在处理序列数据时不会改变其形状,而是在相同的形状下进行信息转换

为什么保持形状不变很重要?

  • 这是 变换器架构的核心设计理念,使其能够适用于 各种序列到序列(Sequence-to-Sequence)任务
  • 每个输出向量 直接 对应一个输入向量,保持 一对一关系
  • 尽管物理维度(sequence length & feature size) 不变,但 输出向量的内容已经重新编码,并整合了整个输入序列的上下文信息(详见 第3章)。

完成 G P T GPT GPT 架构的所有构建模块
在本节中,我们成功实现了 变换器块,至此,我们已经具备了 构建 G P T GPT GPT架构所需的全部组件,如 图4.14 所示。


在这里插入图片描述图4.14 展示了 本章已实现的所有核心概念,这些构建模块将在下一节 完整地组合为 G P T GPT GPT模型


图4.14 所示,变换器块(Transformer Block) 结合了多个关键组件,包括:

  • 层归一化(Layer Normalization)
  • 前馈网络(Feed Forward Network)(包含 GELU激活函数
  • 捷径连接(Shortcut Connections)

这些组件我们已经在本章的前面部分进行了介绍和实现。

在下一章中,我们将看到 变换器块 将作为 G P T GPT GPT架构的主要组成部分,并用于构建完整的 G P T GPT GPT模型。

4.6 编写GPT模型

本章开始时,我们对 GPT架构 进行了整体概述,并实现了一个 占位版本(DummyGPTModel)。在 DummyGPTModel 代码实现中,我们展示了 GPT模型的输入和输出,但其内部构建模块仍然是一个 黑盒,使用了 DummyTransformerBlockDummyLayerNorm 作为占位符。

在本节中,我们将 用真实的TransformerBlockLayerNorm 替换这些 占位符,从而 组装出完整可运行的 G P T GPT GPT-2模型(1.24亿参数版本)

第5章,我们将 预训练(Pretrain) G P T GPT GPT-2模型,而在 第6章,我们将 加载来自 O p e n A I OpenAI OpenAI的预训练权重

GPT-2 模型整体结构

在正式编写 GPT-2模型代码 之前,让我们先看 图4.15,它展示了 GPT模型的整体架构,并汇总了本章介绍的所有核心概念。


在这里插入图片描述图4.15GPT模型架构概览

  • 数据流(data flow) 从底部开始
  1. 标记化文本(Tokenized Text) 转换为 标记嵌入(Token Embeddings)
  2. 标记嵌入 再与 位置嵌入(Positional Embeddings) 结合,以编码序列信息。
  3. 这些 组合信息 形成 张量(Tensor),并输入到 变换器块(Transformer Blocks) 进行处理。
  • 中间部分(核心计算)
  1. 变换器块(Transformer Blocks)多个层堆叠组成,每个变换器块包括:
    • 多头注意力(Multi-Head Attention)
    • 前馈神经网络(Feed Forward Neural Network, FFN)
    • 层归一化(Layer Normalization)
    • 随机失活(Dropout)
  2. 变换器块被堆叠并重复12次(即 G P T GPT GPT-2的12层架构)。

在接下来的代码部分,我们将正式实现 完整的 G P T GPT GPT-2架构

图4.15 所示,在 G P T GPT GPT模型架构 中,我们在 4.5节 编写的 变换器块(Transformer Block) 会被多次重复

  • 对于 G P T GPT GPT-2的1.24亿参数版本,该 变换器块重复12次,这一数值由 GPT_CONFIG_124M["n_layers"] = 12 指定。
  • 对于 G P T GPT GPT-2的最大版本(15.42亿参数),该 变换器块重复36次

最终的输出层

图4.15 所示:

  1. 最后一个变换器块的输出 经过 最终的层归一化(Layer Normalization)
  2. 随后进入最终的线性输出层(Linear Output Layer),该层将 变换器的输出投影到高维空间
    • G P T GPT GPT-2 中,这个维度是 50,257,对应 模型的词汇表大小(vocabulary size)
  3. 最终输出预测序列中的下一个标记(token)

现在,我们来 编写代码,实现 图4.15所示的 G P T GPT GPT模型架构

代码清单 4.7 G P T GPT GPT模型架构的实现

class GPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        '''创建一个词嵌入层,将输入的 token 索引映射到一个固定维度的向量空间。'''
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        '''创建一个位置嵌入层,将每个位置映射到一个固定维度的向量。'''
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        '''构造一个 Dropout 层,用于在训练时随机丢弃部分神经元,防止过拟合。'''
        self.drop_emb = nn.Dropout(cfg["drop_rate"])
        
        # Use a placeholder for TransformerBlock
        '''
        * 操作符的作用是将列表中的元素拆开,为“解包”操作符。
        将这个列表中的所有元素“解包”,使它们变成 nn.Sequential 的单独参数。
        等效于写成(假设cfg["n_layers"] = 2):
        self.trf_blocks = nn.Sequential(TransformerBlock(cfg), TransformerBlock(cfg))
        创建一个由多个 TransformerBlock 组成的序列,然后用 nn.Sequential 将它们串联在一起。
        '''
        self.trf_blocks = nn.Sequential(*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])
        
        # Use a placeholder for LayerNorm
        '''
        创建一个层归一化层,通常用于稳定训练,但这里使用的是一个占位实现(LayerNorm),实际不做任何操作。
        '''
        self.final_norm = LayerNorm(cfg["emb_dim"])
        '''创建一个线性层,将最后得到的嵌入映射到词汇表大小的维度,输出 logits。'''
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

    def forward(self, in_idx):
        """定义模型的前向传播接口,输入为 token 索引组成的张量 in_idx。"""
        batch_size, seq_len = in_idx.shape
        '''
        将 token 索引转换为对应的嵌入向量。
        输出形状为 (batch_size, seq_len, emb_dim),例如 (2, 6, 8)。
        '''
        tok_embeds = self.tok_emb(in_idx)
        '''
        生成位置序列 [0, 1, ..., seq_len-1] 并查找对应的位置信息向量。
        输出形状为 (seq_len, emb_dim),例如 (6, 8)。
        '''
        pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
        '''
        将 token 嵌入与位置嵌入相加,融合语义信息和位置信息。
        pos_embeds 的形状 (6, 8) 自动扩展到 (2, 6, 8) 与 tok_embeds 相加。
        1. tok_embeds[0](6,8)+pos_embeds(6, 8) ->[0](6,8)
        2. tok_embeds[1](6,8)+pos_embeds(6, 8) ->[1](6,8)
        变成(2, 6, 8)
        '''
        x = tok_embeds + pos_embeds
        x = self.drop_emb(x)
        '''
        将 x 依次传入所有 TransformerBlock 层处理。
        '''
        x = self.trf_blocks(x)
        '''
        对 x 进行层归一化操作,稳定训练。
        '''
        x = self.final_norm(x)
        '''
        通过线性变换,将每个 token 的 8 维向量映射到词汇表大小维度(例如 100),得到每个词的 logits。
        '''
        logits = self.out_head(x)
        return logits

由于我们在 4.5节 实现了 TransformerBlock 类,因此 GPTModel 类的代码相对紧凑且易于理解。

GPTModel 类的 __init__ 构造函数 中:

  • 标记嵌入层(Token Embedding Layer) 和 位置嵌入层(Positional Embedding Layer) 由传入的 配置字典(cfg) 进行初始化。
    • 这些 嵌入层 负责 将输入的标记索引转换为密集向量,并 添加位置信息(详见 第2章)。
  • 随后,创建一系列 TransformerBlock 变换器块,其数量等于 cfg["n_layers"] 中指定的层数。
  • 在变换器块之后,应用层归一化(LayerNorm),标准化变换器块的输出,以 稳定学习过程
  • 最后,定义一个无偏置(bias=False)的线性输出层,它 将变换器的输出投影到词汇空间,用于 计算词汇表中每个标记的对数概率(logits)

前向传播(forward 方法)

  • 接收一个批量输入(batch),其中包含标记索引
  • 计算其标记嵌入,并应用位置嵌入
  • 通过变换器块(Transformer Blocks) 处理序列
  • 对最终输出进行层归一化
  • 计算对数概率(logits),用于预测下一个标记

在下一节中,我们将 把这些对数概率转换为实际的标记(tokens) 和文本输出(text output)

初始化 1.24 亿参数的 GPT 模型

现在,我们使用 GPT_CONFIG_124M 配置字典初始化 G P T GPT GPT-2模型,并输入我们在本章开始时创建的批量文本数据:

torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
out = model(batch)

print("Input batch:\n", batch)
print("\nOutput shape:", out.shape)
print(out)

代码输出:

Input batch:
tensor([[ 6109, 3626, 6100, 345],  # 文本1的标记ID
        [ 6109, 1110, 6622, 257]])  # 文本2的标记ID

Output shape: torch.Size([2, 4, 50257])
tensor([[[ 0.1381,  0.0077, -0.1963,  ..., -0.0222, -0.1060,  0.1717],
         [ 0.3865, -0.8408, -0.6564,  ..., -0.5163,  0.2369, -0.3357],
         [ 0.6989, -0.1829, -0.1631,  ...,  0.1472, -0.6504, -0.0056],
         [-0.4290,  0.1669, -0.1258,  ...,  1.1579,  0.5303, -0.5549]],

        [[ 0.1094, -0.2894, -0.1467,  ..., -0.0557,  0.2911, -0.2824],
         [ 0.0882, -0.3552, -0.3527,  ...,  1.2930,  0.0053,  0.1898],
         [ 0.6091,  0.4702, -0.4094,  ...,  0.7688,  0.3787, -0.1974],
         [-0.0612, -0.0737,  0.4751,  ...,  1.2463, -0.3834,  0.0609]]],
       grad_fn=<UnsafeViewBackward0>)

解析输出

  1. 输入 (batch) 形状为 [2, 4],表示 两个文本样本,每个文本包含 4 个标记(token)
  2. 输出 (out) 形状为 [2, 4, 50257]
    • 2:表示 批量大小(batch size),即两个输入样本。
    • 4:表示 序列长度(sequence length),即每个输入文本包含 4 个标记。
    • 50257:表示 模型词汇表大小(vocabulary size),即 每个标记都有 50257 维的对数概率(logits),用于预测 下一个可能的标记
  3. 最终输出的 tensor
    • 每个标记对应一个 大小为 50257 的对数概率向量(logits vector),其中数值最高的索引即为下一个预测的标记

正如我们所见,输出张量的形状为 [2, 4, 50257],因为我们输入了 2 条文本数据,每条包含 4 个标记(tokens)

  • 最后一维(50257) 对应于标记器的词汇表大小,表示 每个标记都有一个大小为 50257 的对数概率(logits) 向量
  • 在下一节,我们将 把这些 50257 维的输出向量转换回标记(tokens)

计算 GPT-2 的参数总数

在继续转换模型输出为文本之前,我们先花些时间分析 GPT-2 的模型大小。使用 numel() 方法(全称 “number of elements”),我们可以计算 模型所有参数的总数

total_params = sum(p.numel() for p in model.parameters())
print(f"Total number of parameters: {total_params:,}")

输出结果:

Total number of parameters: 163,009,536

为什么 GPT-2 不是 1.24 亿参数,而是 1.63 亿参数?

  • 我们之前提到 GPT-2 的参数量为 1.24 亿,但 实际参数数为 1.63 亿,为什么会有差异?
  • 这是由于 “权重共享(Weight Tying)” 这一概念,该技术被用于 原始 G P T GPT GPT-2架构
  • GPT-2 中,输出层复用了标记嵌入层(Token Embedding Layer) 的权重,这意味着实际存储的参数量要少于模型结构计算出的总参数数

验证 Token 嵌入层 和 输出层 形状

为了理解 权重共享,我们打印 模型的标记嵌入层(tok_emb) 和 输出层(out_head) 的形状

print("Token embedding layer shape:", model.tok_emb.weight.shape)
print("Output layer shape:", model.out_head.weight.shape)

输出结果:

Token embedding layer shape: torch.Size([50257, 768])
Output layer shape: torch.Size([50257, 768])
  • 两者的形状完全相同,都为 [50257, 768],其中:
    • 50257:标记器的 词汇表大小
    • 768:标记嵌入的 维度

这说明 out_head(输出层) 的参数实际上与 tok_emb(标记嵌入层) 的参数是等价的,可以 共享同一组权重

计算去除输出层后,模型的实际可训练参数量

根据权重共享的原则,我们从总参数数中移除输出层的参数

total_params_gpt2 = total_params - sum(p.numel() for p in model.out_head.parameters())
print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")

输出结果:

Number of trainable parameters considering weight tying: 124,412,160

现在,模型的总参数数降至 1.24 亿,与原始 GPT-2 模型的大小匹配

权重共享的意义

  • “权重共享(weight tying)” 减少了模型的整体内存占用(memory footprint) 和计算复杂度(computational complexity)
  • 但在实践中,分开使用独立的 Token 嵌入层 和 输出层,通常会得到更好的训练效果和模型性能
  • 现代 L L M LLM LLM(大型语言模型) 通常不会使用权重共享,而是 使用独立的标记嵌入层和输出层,正如我们在 GPTModel 的实现 中所做的那样。

后续章节
我们将在 第6章 重新讨论 “权重共享(weight tying)”,并在加载 O p e n A I OpenAI OpenAI 预训练的 G P T GPT GPT-2权重 时实现该技术。


练习 4.1:计算前馈网络和多头注意力模块的参数数量

计算并比较:

  • 前馈网络(Feed Forward Module) 中的参数数量。
  • 多头注意力模块(Multi-Head Attention Module) 中的参数数量。

答案

block = TransformerBlock(GPT_CONFIG_124M)
print(block)

total_params = sum(p.numel() for p in block.ff.parameters())
print(f"Total number of parameters in feed forward module: {total_params:,}")
total_params = sum(p.numel() for p in block.att.parameters())
print(f"Total number of parameters in attention module: {total_params:,}")
TransformerBlock(
  (att): MultiHeadAttention(
    (W_query): Linear(in_features=768, out_features=768, bias=False)
    (W_key): Linear(in_features=768, out_features=768, bias=False)
    (W_value): Linear(in_features=768, out_features=768, bias=False)
    (out_proj): Linear(in_features=768, out_features=768, bias=True)
    (dropout): Dropout(p=0.1, inplace=False)
  )
  (ff): FeedForward(
    (layers): Sequential(
      (0): Linear(in_features=768, out_features=3072, bias=True)
      (1): GELU()
      (2): Linear(in_features=3072, out_features=768, bias=True)
    )
  )
  (norm1): LayerNorm()
  (norm2): LayerNorm()
  (drop_shortcut): Dropout(p=0.1, inplace=False)
)
Total number of parameters in feed forward module: 4,722,432
Total number of parameters in attention module: 2,360,064

计算 G P T M o d e l GPTModel GPTModel 的存储需求
接下来,我们计算 G P T M o d e l GPTModel GPTModel(1.63 亿参数)所需的内存

total_size_bytes = total_params * 4  # A: 每个参数占用 4 字节(32-bit float)
total_size_mb = total_size_bytes / (1024 * 1024)  # B: 转换为 MB
print(f"Total size of the model: {total_size_mb:.2f} MB")

输出结果:

Total size of the model: 621.83 MB

结论

  • 假设每个参数都是 32-bit 浮点数(即 4 字节),则 1.63 亿个参数的 G P T M o d e l GPTModel GPTModel 需要 621.83 MB 的存储空间
  • 这说明,即使是 较小的 L L M LLM LLM,也需要相对较大的存储空间

在本节中,我们:

  • 实现了 G P T M o d e l GPTModel GPTModel 架构,并观察到其输出是形状为 [batch_size, num_tokens, vocab_size] 的数值张量。
  • 计算了模型的存储需求,并理解了即使是 小规模的 L L M LLM LLM,也需要较大的存储容量
  • 下一节,我们将 编写代码,将这些输出张量转换为文本

练习 4.2:初始化更大规模的 GPT 模型

本章中,我们初始化了 1.24 亿参数的 G P T GPT GPT模型(GPT-2 Small)
现在,不修改代码结构,仅修改配置文件,使用 GPTModel 实现以下更大规模的 GPT-2 版本:

GPT-2 版本嵌入维度变换器块数量多头注意力头数
GPT-2 Medium10242416
GPT-2 Large12803620
GPT-2 XL16004825

额外任务:计算 每个 G P T GPT GPT模型的总参数量

答案

GPT_CONFIG_124M = {
    "vocab_size": 50257,
    "context_length": 1024,
    "emb_dim": 768,
    "n_heads": 12,
    "n_layers": 12,
    "drop_rate": 0.1,
    "qkv_bias": False
}

def get_config(base_config, model_name="gpt2-small"):
    GPT_CONFIG = base_config.copy()

    if model_name == "gpt2-small":
        GPT_CONFIG["emb_dim"] = 768
        GPT_CONFIG["n_layers"] = 12
        GPT_CONFIG["n_heads"] = 12

    elif model_name == "gpt2-medium":
        GPT_CONFIG["emb_dim"] = 1024
        GPT_CONFIG["n_layers"] = 24
        GPT_CONFIG["n_heads"] = 16

    elif model_name == "gpt2-large":
        GPT_CONFIG["emb_dim"] = 1280
        GPT_CONFIG["n_layers"] = 36
        GPT_CONFIG["n_heads"] = 20

    elif model_name == "gpt2-xl":
        GPT_CONFIG["emb_dim"] = 1600
        GPT_CONFIG["n_layers"] = 48
        GPT_CONFIG["n_heads"] = 25

    else:
        raise ValueError(f"Incorrect model name {model_name}")

    return GPT_CONFIG


def calculate_size(model): # based on chapter code
    
    total_params = sum(p.numel() for p in model.parameters())
    print(f"Total number of parameters: {total_params:,}")

    total_params_gpt2 =  total_params - sum(p.numel() for p in model.out_head.parameters())
    print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")
    
    # Calculate the total size in bytes (assuming float32, 4 bytes per parameter)
    total_size_bytes = total_params * 4
    
    # Convert to megabytes
    total_size_mb = total_size_bytes / (1024 * 1024)
    
    print(f"Total size of the model: {total_size_mb:.2f} MB")

from gpt import GPTModel


for model_abbrev in ("small", "medium", "large", "xl"):
    model_name = f"gpt2-{model_abbrev}"
    CONFIG = get_config(GPT_CONFIG_124M, model_name=model_name)
    model = GPTModel(CONFIG)
    print(f"\n\n{model_name}:")
    calculate_size(model)

下载 gpt.py 到相同目录中。


4.7 生成文本

在本章的最后一节,我们将 实现代码,将 G P T GPT GPT模型的张量输出转换回文本。在开始之前,让我们简要回顾 生成式模型(Generative Model)(如 L L M LLM LLM)是如何 逐步生成文本的,如 图4.16 所示。


在这里插入图片描述图4.16 展示了 L L M LLM LLM 逐步生成文本的过程

  • 模型一次生成一个标记(token)
  • 初始输入上下文(initial input context) “Hello, I am” 为例:
    1. 第一步:模型预测 下一个标记,添加 “a”
    2. 第二步:模型再预测 下一个标记,添加 “model”
    3. 第三步:继续预测 下一个标记,添加 “ready”
  • 最终,模型逐步构建完整的句子

图4.16 直观地展示了 G P T GPT GPT模型 在给定输入上下文(如 “Hello, I am”)后,逐步生成文本的过程

  • 每次迭代输入上下文都会增长,使得模型能够 生成连贯且符合上下文的文本
  • 第6次迭代,模型已经 构造出完整的句子
    “Hello, I am a model ready to help.”

上一节,我们看到 GPTModel 的当前实现 输出形状为 [batch_size, num_tokens, vocab_size] 的张量现在的问题是: G P T GPT GPT模型如何从这些输出张量生成如图4.16所示的文本?

文本生成的关键步骤: G P T GPT GPT模型从输出张量到最终生成文本的过程涉及多个步骤,如 图4.17 所示:

  1. 解码输出张量(将 logits 转换为概率分布)。
  2. 基于概率分布选择标记(可采用贪心搜索、采样、Top-k采样等策略)。
  3. 将选定的标记转换为可读文本

在这里插入图片描述图4.17 详细说明了 G P T GPT GPT模型文本生成的内部机制,展示了 单个标记生成过程(iteration)

  • 首先,将 输入文本编码为标记ID
  • 然后,将标记ID 输入到 G P T GPT GPT模型
  • 最后,模型的输出被 转换回文本,并 追加到原始输入文本,形成新的上下文。

图4.17 详细展示了 GPT模型的“下一个标记生成”过程,即 模型如何在每一步预测下一个标记

单步标记生成过程:每个生成步骤 中:

  1. G P T GPT GPT模型输出一个矩阵,其中包含表示 潜在下一个标记的向量
  2. 提取下一个标记对应的向量,并使用 Softmax函数 将其转换为 概率分布
  3. 在概率分布中找到最大值对应的索引,该索引即 预测的标记ID
  4. 将该标记ID解码回文本,得到 序列中的下一个标记
  5. 将该标记追加到之前的输入序列,形成 新的输入上下文,用于下一次迭代。

通过这种 逐步扩展输入序列 的方式,模型可以 顺序生成文本,从初始上下文构建 连贯的短语和句子

循环迭代生成完整文本: 在实际应用中,我们会 重复此过程(如 图4.16 所示),直到:

  • 达到用户设定的标记数(token count)
  • 遇到特殊终止标记(如<EOS>)

我们可以使用以下代码 实现GPT模型的标记生成过程

代码清单 4.8 GPT模型生成文本的函数

def generate_text_simple(model, idx, max_new_tokens, context_size):  # A
    """
    - model:GPT 语言模型。  
    - idx:当前输入标记序列(张量格式)。  
    - max_new_tokens:要生成的新标记数。  
    - context_size:模型最大可接受的上下文窗口。  
    """
    for _ in range(max_new_tokens):
        ''' 取输入序列的最后 context_size 个标记,确保输入序列不会超出模型最大上下文限制。 '''
        idx_cond = idx[:, -context_size:]  # B
        with torch.no_grad():
            '''
            model(idx_cond) 计算所有时间步的 logits,其中:
            - 形状为 [batch_size, num_tokens, vocab_size]。  
            '''
            logits = model(idx_cond)
        '''取最后一个token(-1) 的 logits ,即预测下一个标记的概率分布。'''
        logits = logits[:, -1, :]  # C
        '''将 logits 转换为概率分布(probas)。 '''
        probas = torch.softmax(logits, dim=-1)  # D
        '''
        选取最高概率的标记索引,即下一个预测标记ID。  
        - 该方法被称为 “贪心解码(Greedy Decoding)”。
        '''
        idx_next = torch.argmax(probas, dim=-1, keepdim=True)  # E
        '''将新标记追加到输入序列,用于下一轮预测。'''
        idx = torch.cat((idx, idx_next), dim=1)  # F
    return idx

上面的代码片段展示了 使用 P y T o r c h PyTorch PyTorch实现的语言模型生成循环
该代码 迭代生成指定数量的新标记(tokens),并且:

  1. 裁剪当前上下文 以适配模型的 最大上下文窗口大小
  2. 计算预测(logits) 并选择 下一个标记

详细步骤

  • (A) generate_text_simple 函数

    • 输入:
      • model G P T GPT GPT 语言模型。
      • idx:当前 输入标记序列(张量格式)。
      • max_new_tokens要生成的新标记数
      • context_size模型最大可接受的上下文窗口
    • 输出:扩展后的新标记序列
  • (B) idx[:, -context_size:]

    • 输入序列的最后 context_size 个标记,确保 输入序列不会超出模型最大上下文限制
  • (C) logits[:, -1, :]

    • model(idx_cond) 计算 所有时间步的 logits,其中:
      • 形状为 [batch_size, num_tokens, vocab_size]
    • 最后一个token(-1) 的 logits,即 预测下一个标记的概率分布
  • (D) torch.softmax(logits, dim=-1)

    • logits 转换为概率分布(probas)
  • (E) torch.argmax(probas, dim=-1, keepdim=True)

    • 选取 最高概率的标记索引,即 下一个预测标记ID
    • 该方法被称为 贪心解码(Greedy Decoding)
  • (F) torch.cat((idx, idx_next), dim=1)

    • 将新标记追加到输入序列,用于下一轮预测。

关于 Softmax 计算的额外说明
generate_text_simple 函数中,我们使用 torch.softmax()logits 转换为概率分布,并用 torch.argmax() 选取最高概率的标记

但实际上:

  • Softmax 是单调函数(monotonic function),即它不会改变输入的相对大小顺序。
  • 因此,直接对 logits 应用 torch.argmax() 也能得到相同的结果

尽管如此,我们仍然 显式地展示 Softmax 计算步骤,以便直观理解 logits → 概率 → 选取最高概率标记 这一完整流程。此外,在 下一章( G P T GPT GPT训练代码部分),我们会引入 更复杂的采样策略,比如:

  • 随机采样(Stochastic Sampling)
  • 温度缩放(Temperature Scaling)
  • Top-k采样(Top-k Sampling)
  • 核采样(Nucleus Sampling, Top-p Sampling)

这些策略可以 避免模型始终选择最高概率标记,从而 提高生成文本的多样性创造性

这个 逐步生成标记ID 并将其追加到上下文的过程,使用 generate_text_simple 函数实现,并在 图4.18 中进一步说明。
(每次迭代中的 标记ID生成过程 详见 图4.17。)


在这里插入图片描述图4.18 展示了 六次迭代的标记预测循环,其中:

  • 模型接收初始标记ID序列作为输入
  • 预测下一个标记将其追加到输入序列,形成新的输入序列,用于下一次迭代。
  • (为便于理解)标记ID 也被转换为对应的文本

图4.18 所示,我们以迭代方式生成标记ID。例如,在 第1次迭代 时:

  • 模型接收 代表 "Hello, I am"输入标记
  • 预测下一个标记(ID为 257,对应 "a")。
  • 将预测出的标记追加到输入序列,形成新的输入。

该过程不断重复,直到 第6次迭代,模型最终生成完整的句子:
“Hello, I am a model ready to help.”

我们现在尝试 "Hello, I am" 作为模型输入,并使用 generate_text_simple 进行文本生成,如 图4.18 所示。

首先,将输入文本编码为标记ID

start_context = "Hello, I am"
encoded = tokenizer.encode(start_context)
print("encoded:", encoded)

encoded_tensor = torch.tensor(encoded).unsqueeze(0)  # A
print("encoded_tensor.shape:", encoded_tensor.shape)

编码后的标记ID

encoded: [15496, 11, 314, 716]
encoded_tensor.shape: torch.Size([1, 4])

接下来,我们将模型置于 eval() 模式(禁用 dropout 等随机成分,仅用于训练时),并使用 generate_text_simple 进行文本生成:

model.eval()  # A
out = generate_text_simple(
    model=model,
    idx=encoded_tensor,
    max_new_tokens=6,
    context_size=GPT_CONFIG_124M["context_length"]
)
print("Output:", out)
print("Output length:", len(out[0]))

生成的标记ID如下

Output: tensor([[15496, 11, 314, 716, 27018, 24086, 47843, 30961, 42348, 7267]])
Output length: 10

将标记ID解码回文本

decoded_text = tokenizer.decode(out.squeeze(0).tolist())
print(decoded_text)

模型输出的文本如下

Hello, I am Featureiman Byeswickattribute argue

为什么生成的文本是无意义的?
从上面的输出可以看出,模型生成的文本是杂乱无章的,完全不像 图4.18 中的连贯文本。

原因模型尚未经过训练!

  • 目前,我们只是实现了 G P T GPT GPT架构,并实例化了一个 G P T GPT GPT模型,但其参数仍然是随机初始化的。
  • 模型训练是一个庞大的主题,我们将在下一章深入探讨如何训练 G P T GPT GPT模型

练习 4.3:使用不同的 dropout 参数

在本章的开头,我们在 GPT_CONFIG_124M 字典中定义了 全局 drop_rate 参数,用于设置 G P T M o d e l GPTModel GPTModel 结构中的所有 dropout

修改代码,使不同的 dropout 层使用独立的 dropout
(提示:模型架构中有三个不同的 dropout):

  1. 嵌入层(Embedding Layer) 的 dropout
  2. 捷径连接(Shortcut Layer) 的 dropout
  3. 多头注意力模块(Multi-Head Attention Module) 的 dropout

任务

  • 修改 GPT_CONFIG_124M,为不同 dropout 位置指定不同的值
  • 更新 GPTModel 代码,使其分别使用这些不同的 dropout 设置

答案

GPT_CONFIG_124M = {
    "vocab_size": 50257,
    "context_length": 1024,
    "emb_dim": 768,
    "n_heads": 12,
    "n_layers": 12,
    "drop_rate_emb": 0.1,        # NEW: dropout for embedding layers
    "drop_rate_attn": 0.1,       # NEW: dropout for multi-head attention  
    "drop_rate_shortcut": 0.1,   # NEW: dropout for shortcut connections  
    "qkv_bias": False
}
import torch.nn as nn
from gpt import MultiHeadAttention, LayerNorm, FeedForward


class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.att = MultiHeadAttention(
            d_in=cfg["emb_dim"],
            d_out=cfg["emb_dim"],
            context_length=cfg["context_length"],
            num_heads=cfg["n_heads"], 
            dropout=cfg["drop_rate_attn"], # NEW: dropout for multi-head attention
            qkv_bias=cfg["qkv_bias"])
        self.ff = FeedForward(cfg)
        self.norm1 = LayerNorm(cfg["emb_dim"])
        self.norm2 = LayerNorm(cfg["emb_dim"])
        self.drop_shortcut = nn.Dropout(cfg["drop_rate_shortcut"])

    def forward(self, x):
        # Shortcut connection for attention block
        shortcut = x
        x = self.norm1(x)
        x = self.att(x)  # Shape [batch_size, num_tokens, emb_size]
        x = self.drop_shortcut(x)
        x = x + shortcut  # Add the original input back

        # Shortcut connection for feed-forward block
        shortcut = x
        x = self.norm2(x)
        x = self.ff(x)
        x = self.drop_shortcut(x)
        x = x + shortcut  # Add the original input back

        return x


class GPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        self.drop_emb = nn.Dropout(cfg["drop_rate_emb"]) # NEW: dropout for embedding layers

        self.trf_blocks = nn.Sequential(
            *[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])

        self.final_norm = LayerNorm(cfg["emb_dim"])
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

    def forward(self, in_idx):
        batch_size, seq_len = in_idx.shape
        tok_embeds = self.tok_emb(in_idx)
        pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
        x = tok_embeds + pos_embeds  # Shape [batch_size, num_tokens, emb_size]
        x = self.drop_emb(x)
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        logits = self.out_head(x)
        return logits
import torch

torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)

4.8 总结

  • 层归一化(Layer Normalization) 通过确保每一层的输出具有一致的 均值(mean) 和 方差(variance)稳定训练过程
  • 捷径连接(Shortcut Connections) 通过 跳过一个或多个层,将某一层的输出直接传递到更深的层,有助于缓解深度神经网络(如 L L M LLM LLM)的梯度消失问题(vanishing gradient problem)
  • 变换器块(Transformer Blocks) G P T GPT GPT模型的核心结构组件结合了掩码多头注意力模块(masked multi-head attention) 和 使用GELU激活函数的全连接前馈网络
  • G P T GPT GPT模型是由多个重复的变换器块组成的 L L M LLM LLM,参数量从百万到数十亿
  • G P T GPT GPT模型有多个不同的规模,例如 124、345、762 和 1542 百万参数,这些都可以使用相同的 GPTModel Python 类进行实现。
  • G P T GPT GPT L L M LLM LLM的文本生成能力 依赖于 逐步预测单个标记(token) 并解码输出张量为可读文本,基于 给定的输入上下文 进行生成。
  • 未经训练的 G P T GPT GPT模型生成的文本是无意义的,这强调了 模型训练对生成连贯文本的重要性,这一主题将在后续章节中深入探讨。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2318233.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【新能源汽车“心脏”赋能:三电系统研发、测试与应用匹配的恒压恒流源技术秘籍】

新能源汽车“心脏”赋能&#xff1a;三电系统研发、测试与应用匹配的恒压恒流源技术秘籍 在新能源汽车蓬勃发展的浪潮中&#xff0c;三电系统&#xff08;电池、电机、电控&#xff09;无疑是其核心驱动力。而恒压源与恒流源&#xff0c;作为电源管理的关键要素&#xff0c;在…

目标检测20年(一)

今天看的文献是《Object Detection in 20 Years: A Survey》&#xff0c;非常经典的一篇目标检测文献&#xff0c;希望通过这篇文章学习到目标检测的基础方法并提供一些创新思想。 论文链接&#xff1a;1905.05055 一、摘要 1.1 原文 Object detection, as of one the most…

【MySQL数据库】存储过程与自定义函数(含: SQL变量、分支语句、循环语句 和 游标、异常处理 等内容)

存储过程&#xff1a;一组预编译的SQL语句和流程控制语句&#xff0c;被命名并存储在数据库中。存储过程可以用来封装复杂的数据库操作逻辑&#xff0c;并在需要时进行调用。 类似的操作还有&#xff1a;自定义函数、.sql文件导入。 我们先从熟悉的函数开始说起&#xff1a; …

WEB攻防-PHP反序列化-字符串逃逸

目录 前置知识 字符串逃逸-减少 字符串逃逸-增多 前置知识 1.PHP 在反序列化时&#xff0c;语法是以 ; 作为字段的分隔&#xff0c;以 } 作为结尾&#xff0c;在结束符}之后的任何内容不会影响反序列化的后的结果 class people{ public $namelili; public $age20; } var_du…

英伟达GTC 2025大会产品全景剖析与未来路线深度洞察分析

【完整版】3月19日&#xff0c;黄仁勋Nvidia GTC 2025 主题演讲&#xff5c;英伟达 英伟达GTC 2025大会产品全景剖析与未来路线深度洞察分析 一、引言 1.1 分析内容 本研究主要采用了文献研究法、数据分析以及专家观点引用相结合的方法。在文献研究方面&#xff0c;广泛收集了…

基于java的ssm+JSP+MYSQL的九宫格日志网站(含LW+PPT+源码+系统演示视频+安装说明)

系统功能 管理员功能模块&#xff1a; 个人中心 用户管理 日记信息管理 美食信息管理 景点信息管理 新闻推荐管理 日志展示管理 论坛管理 我的收藏管理 管理员管理 留言板管理 系统管理 用户功能模块&#xff1a; 个人中心 日记信息管理 美食信息管理 景点信息…

【Java】Mybatis学习笔记

目录 一.搭建Mybatis 二.Mybatis核心配置文件解析 1.environment标签 2.typeAliases 3.mappers 三.Mybatis获取参数值 四.Mybatis查询功能 五.特殊的SQL执行 1.模糊查询 2.批量删除 3.动态设置表名 4.添加功能获取自增的主键 六.自定义映射ResultMap 1.配置文件处…

遗传算法+四模型+双向网络!GA-CNN-BiLSTM-Attention系列四模型多变量时序预测

遗传算法四模型双向网络&#xff01;GA-CNN-BiLSTM-Attention系列四模型多变量时序预测 目录 遗传算法四模型双向网络&#xff01;GA-CNN-BiLSTM-Attention系列四模型多变量时序预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 基于GA-CNN-BiLSTM-Attention、CNN-BiL…

中兴B860AV3.2-T/B860AV3.1-T2_S905L3-B_2+8G_安卓9.0_先线刷+后卡刷固件-完美修复反复重启瑕疵

中兴电信B860AV3.2-T&#xff0f;B860AV3.1-T2_晶晨S905L3-B芯片_28G_安卓9.0_先线刷后卡刷-刷机固件包&#xff0c;完美修复刷机后盒子反复重启的瑕疵。 这两款盒子是可以通刷的&#xff0c;最早这个固件之前论坛本人以及其他水友都有分享交流过不少的固件&#xff0c;大概都…

《Python实战进阶》No27: 日志管理:Logging 模块的最佳实践(下)

No27: 日志管理&#xff1a;Logging 模块的最佳实践&#xff08;下&#xff09; 实战案例 &#xff1a;复杂场景下的 Logging 配置与使用 本实战案例在 Python 3.11.5环境下运行通过 在本案例中&#xff0c;我们将通过一个复杂的日志配置示例&#xff0c;全面展示 logging 模…

Web 小项目: 网页版图书管理系统

目录 最终效果展示 代码 Gitee 地址 1. 引言 2. 留言板 [热身小练习] 2.1 准备工作 - 配置相关 2.2 创建留言表 2.3 创建 Java 类 2.4 定义 Mapper 接口 2.5 controller 2.6 service 3. 图书管理系统 3.1 准备工作 - 配置相关 3.2 创建数据库表 3.2.1 创建用户表…

【Dive Into Stable Diffusion v3.5】1:开源项目正式发布——深入探索SDv3.5模型全参/LoRA/RLHF训练

目录 1 引言2 项目简介3 快速上手3.1 下载代码3.2 环境配置3.3 项目结构3.4 下载模型与数据集3.5 运行指令3.6 核心参数说明3.6.1 通用参数3.6.2 优化器/学习率3.6.3 数据相关 4 结语 1 引言 在人工智能和机器学习领域&#xff0c;生成模型的应用越来越广泛。Stable Diffusion…

《Waf 火绒终端防护绕过实战:系统程序副本+Certutil木马下载技术详解》

目录 绕过火绒终端安全软件的详细方法 方法一&#xff1a;利用系统程序副本绕过命令监控 方法二&#xff1a;结合certutil.exe副本下载并执行上线木马 注意事项 总结 实际案例解决方案 前提条件 详细操作步骤 1. 攻击主机&#xff08;VPS&#xff09;上的准备工作 2.…

上海高考解析几何

解析几何的核心思想。 1. 核心分析方法&#xff1a; 自由度引入 方程组中&#xff0c; n n n 个未知数需要 n n n 个等式来解出具体的值。 自由度 性质 一个未知数带来一个自由度&#xff0c;一个等式条件减少一个自由度&#xff08;减少自由度的方式为消元&#xff09;。…

【AVRCP】服务发现互操作性:CT 与 TG 的 SDP 协议契约解析

目录 一、服务发现的核心目标&#xff1a;能力画像对齐 二、控制器&#xff08;CT&#xff09;服务记录&#xff1a;控制能力的声明 2.1 必选字段&#xff1a;角色与协议的刚性契约 2.1.1 服务类标识&#xff08;Service Class ID List&#xff09; 2.1.2 协议描述列表&am…

MySQL:数据库基础

数据库基础 1.什么是数据库&#xff1f;2.为什么要学习数据库&#xff1f;3.主流的数据库&#xff08;了解&#xff09;4.服务器&#xff0c;数据库&#xff0c;表之间的关系5.数据的逻辑存储6.MYSQL架构7.存储引擎 1.什么是数据库&#xff1f; 数据库(Database,简称DB)&#x…

深入 Linux 声卡驱动开发:核心问题与实战解析

1. 字符设备驱动如何为声卡提供操作接口&#xff1f; 问题背景 在 Linux 系统中&#xff0c;声卡被抽象为字符设备。如何通过代码让应用程序能够访问声卡的录音和播放功能&#xff1f; 核心答案 1.1 字符设备驱动的核心结构 Linux 字符设备驱动通过 file_operations 结构体定…

OpenNJet动态API设置accessLog开关,颠覆传统运维工作模式

OpenNJet OpenNJet 应用引擎是高性能、轻量级的WEB应用与代理软件。作为云原生服务网格的数据平面&#xff0c;NJet具备动态配置加载、主动式健康检测、集群高可用、声明式API等多种强大功能。通过CoPliot副驾驶服务框架&#xff0c;在隔离控制面和数据面的情况下实现了高可扩…

案例5_4: 6位数码管轮播0-9【静态显示】

文章目录 文章介绍效果图提示代码&#xff08;不完整&#xff09; 文章介绍 5.1.2 数码管静态显示应用举例 要求&#xff1a; 1、仿真图同案例5_3 2、代码参考案例5_3和案例5_2 效果图 提示代码&#xff08;不完整&#xff09; #include<reg52.h> // 头文件#define uch…

navicat忘记已经连接过的数据库密码的操作步骤

第一步&#xff1a; 点击文件-》导出连接 第二步&#xff1a;选中具体的数据库&#xff0c;且勾选左下角的记住密码 第三步&#xff1a;打开刚刚导出的文件&#xff0c;找到对应加密后的密码 第四步&#xff1a;复制密码到工具点击查看密码 注&#xff1a;参考文章链接附…