从零开始逐步指导开发者构建自己的大型语言模型(LLM)学习笔记- 第4章 GPT 的大语言模型架构Implementing a GPT model

从零搭建 GPT 模型:开启文本生成之旅

一、GPT 模型概述

1.1 什么是 GPT 模型

  • GPT,全称为 Generative Pretrained Transformer,是一种基于 Transformer 架构的大型语言模型,旨在生成自然流畅的文本。它通过对海量文本数据的学习,掌握语言的语法、语义和逻辑,能够根据给定的上下文生成连贯的后续文本。

1.2 GPT 模型的应用场景

  • 内容创作:辅助撰写新闻报道、故事、论文大纲等,激发创作灵感,提高写作效率。
  • 智能客服:理解用户问题,提供准确、快速的回答,提升客户服务体验。
  • 智能问答:回答各种领域的知识问题,如历史、科学、技术等。
  • 代码生成:根据需求生成相应的代码片段,助力程序员开发。

二、编码 LLM 架构

2.1 GPT 模型架构剖析

  • 输入层:将文本分词后转换为向量表示,通常采用词嵌入技术,如 Word2Vec 或 BERT 中的嵌入方法。
  • 多层 Transformer:核心组件,由多个 Transformer 块堆叠而成,每个块包含多头注意力机制、前馈网络和层归一化等。
  • 输出层:将 Transformer 输出转换为词汇表上的概率分布,以预测下一个单词。

2.2 构建基础框架

  • 定义模型类,继承自 nn.Module,初始化模型的各层组件。
  • 实现前向传播函数,按照输入层、Transformer 层、输出层的顺序依次传递数据。

三、层归一化(Layer Normalization)

3.1 原理与作用

  • 原理:对神经网络层的激活值进行归一化,使其均值为 0,方差为 1。
  • 作用:稳定训练过程,加快有效权重的收敛速度,减少梯度消失或爆炸问题,提高模型的泛化能力。

3.2 代码实现

  • 计算输入张量在特征维度上的均值和方差。
  • 对输入进行归一化处理,减去均值并除以方差的平方根。
  • 引入可训练的缩放和平移参数,对归一化后的张量进行线性变换。

四、用 GELU 激活函数实现前馈网络

4.1 GELU 激活函数详解

  • 公式:GELU (x) = x * Φ(x),其中 Φ(x) 是标准高斯分布的累积分布函数。
  • 优点:平滑的非线性函数,在输入小于 0 时输出及导数均不为 0,可更新更多参数,提升训练效率;相比 ReLU,参数精细化调整能力更强,训练更稳定。

4.2 前馈网络结构与代码实现

  • 结构:包含两个线性层和一个 GELU 激活函数,第一个线性层扩展输入维度,GELU 激活后,第二个线性层压缩回原维度。
  • 代码:使用 PyTorch 实现,定义前馈网络类,在前向传播中依次应用线性层和 GELU 激活。

五、添加快捷连接(Shortcut Connections)

5.1 快捷连接的意义

  • 缓解梯度消失问题:使得梯度能够更顺畅地在网络中反向传播,避免深层网络中梯度趋近于 0。
  • 增强模型学习能力:让模型能够更快地学习到输入与输出之间的映射关系,加速训练收敛。

5.2 实现方式

  • 在 Transformer 块中,将输入直接加到多头注意力机制和前馈网络的输出上,实现残差连接。

六、在 Transformer 模块中连接注意力和线性层

6.1 多头注意力机制回顾

  • 原理:将输入分别映射到多个子空间,通过并行的注意力头计算注意力得分,再将各头结果拼接并投影回原维度。
  • 作用:捕捉输入序列不同位置之间的复杂关系,增强模型对上下文的理解能力。

6.2 线性层的作用与连接方式

  • 作用:对多头注意力的作输出进行进一步的线性变换,调整特征维度,使其适应后续模块的输入要求。
  • 连接:将多头注意力机制的输出直接输入线性层,通过权重矩阵相乘和偏置相加完成变换。

七、编码 GPT 模型

7.1 整合各组件

  • 将输入嵌入层、多层 Transformer 块(包含注意力、前馈网络、层归一化和快捷连接)、输出层按顺序组合。
  • 确保各组件间的输入输出维度匹配,数据能够顺畅流动。

7.2 模型初始化与参数设置

  • 初始化模型的权重,可采用随机初始化或预训练模型的加载。
  • 设置超参数,如学习率、层数、头数、嵌入维度等,根据任务需求和计算资源进行调优。

八、生成文本

8.1 生成流程

  • 给定初始输入文本,通过分词转换为模型可接受的输入形式。
  • 模型预测下一个单词的概率分布,根据采样策略(如贪心搜索、束搜索等)选择一个单词。
  • 将所选单词添加到输入文本,重复上述步骤,直到生成结束标志或达到最大长度。

8.2 优化策略

  • 温度参数调整:控制生成文本的随机性,温度越高越随机,越低越保守。
  • 束搜索:同时保留多个可能的路径,选择综合得分最高的路径,提高生成质量。

九、总结与展望

9.1 项目总结

  • 回顾从零搭建 GPT 模型的关键步骤,包括架构设计、组件实现、训练与生成。
  • 强调层归一化、GELU 激活、快捷连接等技术对模型性能的提升作用。

9.2 未来展望

  • 探讨 GPT 模型在更多领域的应用潜力,如医疗、金融、教育等。
  • 关注模型的改进方向,如更高效的架构、更强的泛化能力、更好的可解释性。

Chapter 4: Implementing a GPT model from Scratch To Generate Text

整体目标

 

实现一个类似 GPT 的大语言模型架构,并为后续的训练做准备。展示如何构建模型的各个组件,以及如何使用该模型进行简单的文本生成。

代码主要内容

 
  1. 环境信息输出:输出matplotlibtorchtiktoken的版本信息。
  2. GPT 模型架构设计
    • 模型配置:定义了GPT_CONFIG_124M配置字典,包含词汇表大小、上下文长度、嵌入维度、注意力头数、层数、辍学率和查询 - 键 - 值偏差等参数,这些参数用于构建一个小型的 GPT-2 模型。
    • 模型搭建
      • 定义DummyGPTModel类作为模型架构的占位符,其中包含词嵌入、位置嵌入、辍学层、一系列的 Transformer 块(使用占位符DummyTransformerBlock)、层归一化(使用占位符DummyLayerNorm)和输出头。
      • 定义DummyTransformerBlockDummyLayerNorm类作为占位符,它们在forward方法中直接返回输入,不进行实际操作。
    • 模型测试:使用tiktoken库的gpt2编码对文本进行编码,创建一个批次数据,并将其输入到DummyGPTModel中,打印输出形状和结果。
  3. 层归一化(Layer Normalization)
    • 原理介绍:层归一化用于将神经网络层的激活值围绕均值 0 中心化,并将方差归一化到 1,以稳定训练并加速收敛。在 Transformer 块的多头注意力模块前后以及最终输出层之前都会应用层归一化。
    • 代码实现
      • 通过一个简单的神经网络层示例,展示如何计算均值、方差并进行归一化。
      • 定义LayerNorm类,实现层归一化的计算,并添加可训练的scaleshift参数,同时为避免除以零错误添加一个小的eps值。在计算方差时,采用与 GPT-2 训练时一致的有偏估计。
      • 对示例数据应用LayerNorm,验证其效果。
  4. 带有 GELU 激活函数的前馈网络实现
    • 激活函数介绍:在大语言模型中,除了常用的 ReLU 激活函数,还会使用 GELU 和 SwiGLU 等更复杂的激活函数。GELU 是一种平滑的非线性函数,近似于 ReLU 但在负值时具有非零梯度。
    • 代码实现
      • 定义GELU类,实现 GELU 激活函数的近似计算。
      • 绘制 GELU 和 ReLU 激活函数的图像进行对比。
      • 定义FeedForward类,实现前馈网络模块,该模块由线性层、GELU 激活函数和另一个线性层组成。
      • 对随机输入数据应用FeedForward模块,验证其输出形状。
  5. 添加快捷连接(Shortcut Connections)
    • 原理介绍:快捷连接(也称为跳跃连接或残差连接)最初用于计算机视觉的深度网络中,以缓解梯度消失问题。它通过将一层的输出添加到后面一层的输出,为梯度提供了一条更短的路径。
    • 代码实现
      • 定义ExampleDeepNeuralNetwork类,通过设置use_shortcut参数来控制是否使用快捷连接。
      • 定义print_gradients函数,用于计算并打印模型参数的梯度。
      • 分别创建不使用和使用快捷连接的模型实例,对输入数据进行前向和反向传播,打印梯度值,对比结果表明快捷连接可以防止早期层的梯度消失。
  6. Transformer 块的实现
    • 原理介绍:Transformer 块结合了多头注意力模块、线性层、前馈神经网络、辍学和快捷连接等组件。
    • 代码实现
      • 从之前的章节导入MultiHeadAttention模块(假设previous_chapters模块已定义)。
      • 定义TransformerBlock类,包含多头注意力模块、前馈网络、两个层归一化层和一个辍学层。在forward方法中,对注意力模块和前馈网络分别应用快捷连接。
      • 对随机输入数据应用TransformerBlock,验证输入输出形状。
  7. GPT 模型的实现
    • 模型整合:定义GPTModel类,将词嵌入、位置嵌入、辍学层、一系列的TransformerBlock、最终的层归一化和输出头整合到一起,形成完整的 GPT 模型架构。
    • 模型参数与内存计算
      • 实例化GPTModel,计算模型的总参数数量,发现其与 124M 参数的 GPT-2 模型不一致,原因是原始 GPT-2 模型使用了权重绑定(weight tying),即重用词嵌入层作为输出层。通过减去输出层的参数数量,可以得到 124M 参数的模型。
      • 计算模型的内存需求,假设使用 32 位浮点数(每个参数 4 字节),将总参数数量转换为兆字节。
    • 不同配置参考:列出了 GPT-2 模型的不同配置,包括GPT2-smallGPT2-mediumGPT2-largeGPT2-XL,每个配置具有不同的嵌入维度、层数和注意力头数。
  8. 文本生成
    • 贪心解码原理:定义generate_text_simple函数,实现贪心解码方法进行文本生成。在每一步中,模型选择具有最高概率的词(或令牌)作为下一个输出。
    • 生成示例:对输入文本进行编码,将编码后的张量输入到模型中,使用generate_text_simple函数生成指定数量的新令牌,并将输出解码为文本。由于模型未训练,生成的文本是随机的。
  • In this chapter, we implement a GPT-like LLM architecture; the next chapter will focus on training this LLM

Image

4.1 Coding an LLM architecture

  • 这段话提到了一些关于大型语言模型(LLMs)的关键点。首先,GPT和Llama是基于原始Transformer架构的解码器部分构建的,它们按顺序生成单词,因此被称为“类解码器”的LLMs。与传统的深度学习模型相比,LLMs之所以更大,主要是因为其参数数量庞大,而非代码量。此外,LLMs的架构中有许多重复的元素。

    简单来说,GPT和Llama这类模型通过模仿人类语言的生成方式来工作,它们的规模庞大,主要是因为需要大量的参数来学习和理解语言的复杂性。而且,这些模型在设计上有很多相似的部分,这种重复性有助于提高模型的效率和性能。

Image

  • Configuration details for the 124 million parameter GPT-2 model include:
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
}
  • We use short variable names to avoid long lines of code later
  • 这段文本主要介绍了一些变量的含义:
    - “vocab_size”表示词汇量大小为 50257 个单词,由第二章讨论的字节对编码(BPE)分词器支持。
    - “context_length”代表模型的最大输入令牌计数,由第二章介绍的位置嵌入实现。
    - “emb_dim”是令牌输入的嵌入大小,将每个输入令牌转换为 768 维向量。
    - “n_heads”是第三章实现的多头注意力机制中的注意力头数量。
    - “n_layers”是模型中的变压器块数量,将在后续部分实现。
    - “drop_rate”是 dropout 机制的强度,在第三章讨论过;0.1 意味着在训练期间丢弃 10%的隐藏单元以减轻过拟合。
    - “qkv_bias”决定在多头注意力机制(来自第三章)中的线性层在计算查询(Q)、键(K)和值(V)张量时是否应包括偏差向量;我们将禁用此选项,这是现代大型语言模型的标准做法;然而,在第五章将 OpenAI 的预训练 GPT-2 权重加载到我们的重新实现中时,我们将重新审视此选项。

Image

import torch
import torch.nn as nn


class DummyGPTModel(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"])
        
        # Use a placeholder for TransformerBlock
        self.trf_blocks = nn.Sequential(
            *[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])])
        
        # Use a placeholder for LayerNorm
        self.final_norm = DummyLayerNorm(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
        x = self.drop_emb(x)
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        logits = self.out_head(x)
        return logits


class DummyTransformerBlock(nn.Module):
    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):
    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
  1. def __init__(self, cfg):

    • 这是构造函数的定义,它接受一个参数 cfg,这是一个配置字典,包含了模型的各种参数设置。
  2. super().__init__()

    • 这行代码调用了父类 nn.Module 的构造函数,确保父类的初始化过程被正确执行。
  3. self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])

    • 这行代码创建了一个词嵌入层 self.tok_emb,它将输入的词汇索引(token indices)映射到一个低维的向量空间中。nn.Embedding 是 PyTorch 中的一个模块,它接受两个参数:词汇表的大小 vocab_size 和嵌入向量的维度 emb_dim
  4. self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])

    • 这行代码创建了一个位置嵌入层 self.pos_emb,它将输入序列中的每个位置映射到一个低维的向量空间中。nn.Embedding 的参数与词嵌入层相同,context_length 表示输入序列的最大长度。
  5. self.drop_emb = nn.Dropout(cfg["drop_rate"])

    • 这行代码创建了一个 Dropout 层 self.drop_emb,它在训练过程中随机将一部分神经元的输出置为零,以防止过拟合。cfg["drop_rate"] 是 Dropout 的概率,即被置为零的神经元的比例。
  6. self.trf_blocks = nn.Sequential(*[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])])

    • 这行代码创建了一个由多个 DummyTransformerBlock 组成的序列 self.trf_blocksnn.Sequential 是 PyTorch 中的一个模块,它将多个模块按顺序组合在一起,形成一个新的模块。DummyTransformerBlock 是一个简化版的 Transformer 块,它在这个模型中只是一个占位符,不执行任何实际的计算。cfg["n_layers"] 表示 Transformer 块的数量。
  7. self.final_norm = DummyLayerNorm(cfg["emb_dim"])

    • 这行代码创建了一个 LayerNorm 层 self.final_norm,它对输入进行归一化处理。DummyLayerNorm 是一个简化版的 LayerNorm 层,它在这个模型中也是一个占位符,不执行任何实际的计算。cfg["emb_dim"] 是输入的特征维度。
  8. self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

    • 这行代码创建了一个线性层 self.out_head,它将输入的特征映射到词汇表的大小,用于生成预测的词汇概率分布。nn.Linear 是 PyTorch 中的一个模块,它接受两个参数:输入特征的维度 emb_dim 和输出特征的维度 vocab_sizebias=False 表示不使用偏置项。

Image

这段代码使用 PyTorch 设置随机种子为 123,创建一个名为 “DummyGPTModel” 的模型实例(假设 “GPT_CONFIG_124M” 是模型的配置参数)。然后将一个名为 “batch” 的数据【batch.append(torch.tensor(tokenizer.encode(Every effort moves you)))】传入模型进行计算,得到输出的 logits。接着打印出输出的形状和具体的 logits 值。

Note

  • If you are running this code on Windows or Linux, the resulting values above may look like as follows:
  • Since these are just random numbers, this is not a reason for concern, and you can proceed with the remainder of the chapter without issues

4.2 Normalizing activations with layer normalization

  • 层归一化(LayerNorm)是一种对神经网络层的激活值进行处理的手段。

    **一、基本原理**
    1. 它将层的激活值中心化到均值为0,方差归一化为1。这就好比把一群分散的数据按照一定规则整理,让数据的分布更规整。
    2. 这种规整作用能稳定训练过程。例如在训练神经网络时,数据波动小了,模型就能更平稳地学习,从而更快收敛到较好的权重。

    **二、应用位置**
    1. 在Transformer块中,多头的注意力模块前后都会用到层归一化。
    2. 在最后的输出层之前也会应用。这是因为在这些关键位置进行层归一化有助于提高整个模型的性能,保证数据在不同阶段的合理性和有效性。

Image

  • Let's see how layer normalization works by passing a small input sample through a simple neural network layer:
  • The normalization is applied to each of the two inputs (rows) independently; using dim=-1 applies the calculation across the last dimension (in this case, the feature dimension) instead of the row dimension

Image

  • 这是一种数据标准化的操作。首先减去均值,这一步能让数据的中心位置移到0。比如一组数[1,3,5],均值为3,减去均值后变为[-2,0,2],此时中心就在0了。然后除以标准差(方差的平方根),这是为了调整数据的离散程度,让方差变为1。这样做有很多好处,在数据分析、机器学习等领域,不同特征的数值范围可能差异很大,通过这种标准化处理后,各个特征在列(特征维度)上就有相同的尺度了,模型能更好地处理数据,避免因数值大小差异造成的偏差等问题。
 
  • Above, we normalized the features of each input
  • Now, using the same idea, we can implement a LayerNorm class:
  • 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

Scale and shift

  • “Scale and shift” 即 “缩放和平移”。在文中,除了通过减去均值并除以方差进行归一化之外,还添加了两个可训练的参数,即 “scale”(缩放参数)和 “shift”(平移参数)。
  • 初始的 “scale”(乘以 1)和 “shift”(加上 0)值没有任何影响。
  • 然而,“scale” 和 “shift” 是可训练的参数,语言模型在训练过程中如果确定调整它们可以提高模型在训练任务上的性能,就会自动调整。
  • 这使得模型能够学习到最适合它正在处理的数据的适当缩放和平移。另外,在计算方差的平方根之前添加一个较小的值(eps),是为了避免方差为 0 时出现除零错误。

Biased variance

 

Image

4.3 Implementing a feed forward network with GELU activations

  •  GELU 激活函数的前馈网络方面的内容。首先提到在深度学习中,ReLU 激活函数因简单有效而被广泛使用,但在语言模型中会使用 GELU 和 SwiGLU 等其他激活函数。GELU 和 SwiGLU 更复杂且性能更好,GELU 定义为 GELU (x)=x⋅Φ(x),其中 Φ(x) 是标准高斯分布的累积分布函数。实际中常使用一种计算成本更低的近似公式,并且提到原始的 GPT-2 模型也是用这个近似公式训练的。
  • In practice, it's common to implement a computationally cheaper approximation: \text{GELU}(x) \approx 0.5 \cdot x \cdot \left(1 + \tanh\left[\sqrt{\frac{2}{\pi}} \cdot \left(x + 0.044715 \cdot x^3\right)\right]\right) (the original GPT-2 model was also trained with this approximation)

 x = torch.linspace(-3, 3, 100) 用于创建一个一维张量 x,该张量包含了从 -3 到 3 之间的 100 个等间隔的数值。

以下是对这段代码的详细解释:

  • torch.linspace(start, end, steps) 是 PyTorch 中的一个函数,用于生成一个从 start 到 end 的等间隔的一维张量。
  • start 和 end 分别指定了张量的起始值和结束值。
  • steps 指定了生成的张量中元素的数量。
  • 绘制 GELU 和 ReLU 激活函数的图像。以下是对这段代码的详细解释:

  • plt.figure(figsize=(8, 3))

    • 这行代码创建了一个新的图形窗口,并设置了图形的大小为 8x3 英寸。figsize 参数用于指定图形的宽度和高度。
  • for i, (y, label) in enumerate(zip([y_gelu, y_relu], ["GELU", "ReLU"]), 1):

    • 这是一个循环,用于遍历 GELU 和 ReLU 激活函数的输出以及对应的标签。zip([y_gelu, y_relu], ["GELU", "ReLU"]) 将两个列表 [y_gelu, y_relu] 和 ["GELU", "ReLU"] 打包成一个元组的列表,每个元组包含一个激活函数的输出和对应的标签。enumerate(zip([y_gelu, y_relu], ["GELU", "ReLU"]), 1) 对这个元组列表进行枚举,并从索引 1 开始。
  • plt.subplot(1, 2, i)

    • 这行代码在当前图形窗口中创建一个子图。subplot(1, 2, i) 表示创建一个 1x2 的子图网格,并选择第 i 个子图。i 是循环变量,表示当前正在处理的激活函数的索引。
  • plt.plot(x, y)

    • 这行代码在当前子图中绘制激活函数的图像。x 是输入数据,y 是激活函数的输出。
  • plt.title(f"{label} activation function")

    • 这行代码为当前子图设置标题,标题显示激活函数的名称。
  • plt.xlabel("x")

    • 这行代码为当前子图设置 x 轴标签,标签为 "x"。
  • plt.ylabel(f"{label}(x)")

    • 这行代码为当前子图设置 y 轴标签,标签为激活函数的名称加上 "(x)"。
  • plt.grid(True)

    • 这行代码在当前子图中显示网格线。
  • plt.tight_layout()

    • 这行代码调整子图的布局,使它们在图形窗口中更紧凑地排列。

在这个例子中,start 是 -3,end 是 3,steps 是 100,因此生成的张量 x 将包含从 -3 到 3 之间的 100 个等间隔的数值。

生成的张量 x 可以用于后续的计算或可视化,例如作为激活函数的输入,或者作为绘图的 x 轴数据。

  • 首先,提到 ReLU 是一个分段线性函数,如果输入为正,则直接输出输入值;如果输入为负,则输出零。接着介绍 GELU 是一个平滑的非线性函数,近似于 ReLU,但在负值时有非零梯度(大约在 - 0.75 处除外)。

  • 最后说明接下来要实现名为 FeedForward 的小神经网络模块,并且指出这个模块在语言模型的 transformer 块中的用途。

Image

[17]

ffn = FeedForward(GPT_CONFIG_124M)

# input shape: [batch_size, num_token, emb_size]
x = torch.rand(2, 3, 768)
out = ffn(x)
print(out.shape)

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

首先定义了一个名为ffn的变量,它是通过调用FeedForward函数并传入GPT_CONFIG_124M参数得到的。接着创建了一个形状为(2, 3, 768)的随机张量x,表示有 2 个样本,每个样本有 3 个标记,每个标记的嵌入维度为 768。然后将x传入ffn进行前向传播,得到输出out,最后打印出out的形状。 

Image

Image

4.4 Adding shortcut connections

4.4 添加快捷连接:首先,来谈谈快捷连接(也称为跳过连接或残差连接)背后的概念。最初,快捷连接在用于计算机视觉的深度网络(残差网络)中被提出,目的是缓解梯度消失问题。快捷连接创建了一个可供梯度在网络中流动的更短的替代路径。这是通过将一层的输出添加到后面一层的输出上来实现的,通常会跳过中间的一层或多层。让我们用一个小型示例网络来说明这个想法。

Image

  • In code, it looks like this:
  • class ExampleDeepNeuralNetwork(nn.Module):
        def __init__(self, layer_sizes, use_shortcut):
            super().__init__()
            self.use_shortcut = use_shortcut
            self.layers = nn.ModuleList([
                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:
                # Compute the output of the current layer
                layer_output = layer(x)
                # Check if shortcut can be applied
                if self.use_shortcut and x.shape == layer_output.shape:
                    x = x + layer_output
                else:
                    x = layer_output
            return x
    
    
    def print_gradients(model, x):
        # Forward pass
        output = model(x)
        target = torch.tensor([[0.]])
    
        # Calculate loss based on how close the target
        # and output are
        loss = nn.MSELoss()
        loss = loss(output, target)
        
        # Backward pass to calculate the gradients
        loss.backward()
    
        for name, param in model.named_parameters():
            if 'weight' in name:
                # Print the mean absolute gradient of the weights
                print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")

  • 代码的详细解释:

  • layer_sizes = [3, 3, 3, 3, 3, 1]

    • 这行代码定义了一个列表 layer_sizes,它包含了神经网络每一层的神经元数量。具体来说,这个神经网络有6层,输入层有3个神经元,隐藏层有4层,每层有3个神经元,输出层有1个神经元。
  • sample_input = torch.tensor([[1., 0., -1.]])

    • 这行代码创建了一个张量 sample_input,它包含了一个示例输入数据。这个张量的形状是 (1, 3),表示有1个样本,每个样本有3个特征。
  • torch.manual_seed(123)

    • 这行代码设置了 PyTorch 的随机种子为123。设置随机种子可以确保每次运行代码时,随机数生成器生成的随机数序列是相同的,这对于结果的可重复性非常重要。
  • model_without_shortcut = ExampleDeepNeuralNetwork(layer_sizes, use_shortcut=False)

    • 这行代码创建了一个 ExampleDeepNeuralNetwork 类的实例 model_without_shortcut。这个类是一个自定义的神经网络类,它接受两个参数:layer_sizes 和 use_shortcutlayer_sizes 是一个列表,指定了每一层的神经元数量;use_shortcut 是一个布尔值,指定是否使用残差连接(shortcut connection)。在这个例子中,use_shortcut 被设置为 False,表示不使用残差连接。
  • print_gradients(model_without_shortcut, sample_input)

    • 这行代码调用了一个名为 print_gradients 的函数,它接受两个参数:model_without_shortcut 和 sample_input。这个函数的作用是计算并打印模型在给定输入下的梯度。具体来说,它会计算模型的输出相对于每个可学习参数(权重和偏置)的梯度,并将这些梯度打印出来。
  • 根据上面的输出我们可以看到,shortcut connections(快捷连接)能够防止早期层(朝向 layer.0)的梯度消失。我们接下来在实现一个 transformer block(Transformer 模块)时会使用快捷连接这个概念。
    def forward(self, x):
        """
        定义模型的前向传播过程

        参数:
        x (torch.Tensor): 输入张量

        返回:
        torch.Tensor: 模型的输出张量
        """
        for layer in self.layers:
            # Compute the output of the current layer
            layer_output = layer(x)
            # Check if shortcut can be applied
            if self.use_shortcut and x.shape == layer_output.shape:
                # 如果启用了shortcut并且输入和输出的形状相同,则应用shortcut
                print('x:',x)
                print('layer_output:',layer_output)
                x = x + layer_output
            else:
                # 如果不满足shortcut的条件,则直接使用当前层的输出
                x = layer_output
        return x
---------------------------------------------------
explain:

layer_output = layer(x)

这行代码计算当前层的输出。layer 是 self.layers 中的一个层,x 是输入张量。layer(x) 表示将输入张量 x 传递给当前层进行计算,并将结果存储在 layer_output 中。
if self.use_shortcut and x.shape == layer_output.shape:

这是一个条件语句,检查是否满足两个条件:
self.use_shortcut 为 True,表示启用了残差连接(shortcut connection)。
x 的形状与 layer_output 的形状相同,这是使用残差连接的前提条件,因为只有在输入和输出的形状相同时,才能进行元素级的加法操作。
x = x + layer_output

如果满足上述条件,这行代码将输入张量 x 与当前层的输出 layer_output 进行元素级的加法操作,实现残差连接。这是一种常见的技术,用于缓解深度神经网络中的梯度消失问题,允许信息直接从较早的层传递到较晚的层。

 

4.5 Connecting attention and linear layers in a transformer block

  • 在这一部分,我们现在将前面的概念组合成一个所谓的变压器块。一个变压器块将前一章中的因果多头注意力模块与线性层、我们在前面部分实现的前馈神经网络结合起来。此外,变压器块还使用了丢弃法和快捷连接。
[21]
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"])
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

Image

  • 输入部分
    • 左下角提到 “要嵌入的输入标记(The input tokens to be embedded)”,并给出了几个单词及其对应的嵌入向量示例,如 “Every” 对应[0.2961,..., 0.4604]等。每个单词的嵌入向量是一个 768 维的向量,图中说明 “每一行是一个表示嵌入输入标记的 768 维向量(Each row is a 768 - dimensional vector representing an embedded input token)”。
  • Transformer 模块内部结构
    • 模块主要包含以下几个部分(从下往上):
      • LayerNorm 1:对输入进行层归一化。
      • Masked multi - head attention:掩码多头注意力机制,是 Transformer 的核心组件之一,用于处理输入序列的注意力计算。
      • Dropout:防止过拟合的一种技术,随机丢弃一些神经元。
      • LayerNorm 2:再次进行层归一化。
      • Feed forward:前馈神经网络,图中右侧给出了其内部结构,包含两个线性层(Linear layer)和一个 GELU 激活函数(GELU activation)。
      • Dropout:再次使用 Dropout。
    • 模块中还提到了 “捷径连接(Shortcut connection)”,这是一种残差连接方式,有助于梯度传播和模型训练。
  • 输出部分
    • 顶部显示了输出的形式和维度与输入相同,给出了输出的示例,如[[-0.0256,..., 0.6890], [-0.0178,..., 0.7431], [0.4558,..., 0.7814], [0.0702,..., 0.7134]]
[22]

Image

4.6 Coding the GPT model

  • We are almost there: now let's plug in the transformer block into the architecture we coded at the very beginning of this chapter so that we obtain a usable GPT architecture
  • Note that the transformer block is repeated multiple times; in the case of the smallest 124M GPT-2 model, we repeat it 12 times:

Image

  • The corresponding code implementation, where cfg["n_layers"] = 12:
  • Using the configuration of the 124M parameter model, we can now instantiate this GPT model with random initial weights as follows:
  • 我们将在下一章中训练这个模型。然而,关于它的大小有一个快速说明:我们之前称它为一个有 1.24 亿参数的模型;我们可以如下方式再次核对这个数字。
  • 生成器表达式 sum(p.numel() for p in model.parameters()) 来遍历模型的所有参数,并计算每个参数的元素数量(即参数的总数)。p.numel() 是 PyTorch 中张量(Tensor)的一个方法,用于返回张量中元素的总数。model.parameters() 是模型的一个属性,它返回一个迭代器,遍历模型的所有可学习参数(即权重和偏置)。
  • 如我们在上面看到的,这个模型有 1.63 亿个参数,而不是 1.24 亿个参数;为什么呢?在原始的 GPT-2 论文中,研究人员应用了权重绑定,这意味着他们重复使用了标记嵌入层(tok_emb)作为输出层,也就是设置 self.out_head.weight = self.tok_emb.weight。标记嵌入层将 50257 维的独热编码输入标记投影到 768 维的嵌入表示。输出层将 768 维的嵌入重新投影回 50257 维的表示,这样我们就可以将这些重新转换为单词(在下一节中有更多关于这方面的内容)。所以,嵌入层和输出层具有相同数量的权重参数,正如我们可以根据它们的权重矩阵的形状看到的那样。然而,关于它的大小有一个快速说明:我们之前将其称为一个 1.24 亿参数的模型;我们可以如下方式再次检查这个数字。
[26]
  • 在原始的 GPT-2 论文中,研究人员将标记嵌入矩阵重复用作输出矩阵。相应地,如果我们减去输出层的参数数量,我们将得到一个具有 1.24 亿参数的模型。
[27]

这段代码的作用是计算 GPT-2 模型中除了输出层头部参数之外的可训练参数总数,并打印出考虑权重绑定后的可训练参数数量。

首先,使用`total_params`减去模型输出层头部参数的数量总和。这里通过`sum(p.numel() for p in model.out_head.parameters())`来计算输出层头部参数的总数,其中`p.numel()`表示参数的数量,对模型输出层头部的每个参数进行遍历求和。

然后,将计算得到的结果赋值给`total_params_gpt2`。

最后,使用`print`函数打印出格式化后的信息,显示考虑权重绑定后的可训练参数数量,格式为带有逗号分隔的数字。

在实践中,我发现不进行权重绑定训练模型更容易,这就是为什么我们在这里没有实现它。然而,在第 5 章加载预训练权重时,我们将重新审视并应用这个权重绑定的想法。最后,我们可以如下计算模型的内存需求,这可以作为一个有用的参考点。
  • Exercise: you can try the following other configurations, which are referenced in the GPT-2 paper, as well.

    • GPT2-small (the 124M configuration we already implemented):

      • "emb_dim" = 768
      • "n_layers" = 12
      • "n_heads" = 12
    • GPT2-medium:

      • "emb_dim" = 1024
      • "n_layers" = 24
      • "n_heads" = 16
    • GPT2-large:

      • "emb_dim" = 1280
      • "n_layers" = 36
      • "n_heads" = 20
      • GPT2_large_CONFIG = {
            "vocab_size": 50257,    # Vocabulary size
            "context_length": 1024, # Context length
            "emb_dim": 1280,         # Embedding dimension
            "n_heads": 20,          # Number of attention heads
            "n_layers": 36,         # Number of layers
            "drop_rate": 0.1,       # Dropout rate
            "qkv_bias": False       # Query-Key-Value bias
        }
        torch.manual_seed(123)
        model = GPTModel(GPT2_large_CONFIG)
        
        total_params = sum(p.numel() for p in model.parameters())
        print(f"Total number of parameters: {total_params:,}")
        print("Token embedding layer shape:", model.tok_emb.weight.shape)
        print("Output layer shape:", model.out_head.weight.shape)
        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:,}")
        -------------------------
        Total number of parameters: 838,220,800
        Token embedding layer shape: torch.Size([50257, 1280])
        Output layer shape: torch.Size([50257, 1280])
        Number of trainable parameters considering weight tying: 773,891,840

    • GPT2-XL:

      • "emb_dim" = 1600
      • "n_layers" = 48
      • "n_heads" = 25

4.7 Generating text

  • LLMs like the GPT model we implemented above are used to generate one word at a time

Image

  • 以下的 generate_text_simple 函数实现了贪心解码,这是一种简单且快速的生成文本的方法。在贪心解码中,在每一步,模型选择具有最高概率的单词(或标记)作为其下一个输出(最高的对数几率对应最高的概率,所以实际上我们甚至不必明确地计算 softmax 函数)。在下一章中,我们将实现一个更高级的 generate_text 函数。下面的图描绘了 GPT 模型在给定输入上下文的情况下如何生成下一个单词标记。

Image

[29]
def generate_text_simple(model, idx, max_new_tokens, context_size):
    # idx is (batch, n_tokens) array of indices in the current context
    for _ in range(max_new_tokens):
        
        # Crop current context if it exceeds the supported context size
        # E.g., if LLM supports only 5 tokens, and the context size is 10
        # then only the last 5 tokens are used as context
        idx_cond = idx[:, -context_size:]
        
        # Get the predictions
        with torch.no_grad():
            logits = model(idx_cond)
        
        # Focus only on the last time step
        # (batch, n_tokens, vocab_size) becomes (batch, vocab_size)
        logits = logits[:, -1, :]  

        # Apply softmax to get probabilities
        probas = torch.softmax(logits, dim=-1)  # (batch, vocab_size)

        # Get the idx of the vocab entry with the highest probability value
        idx_next = torch.argmax(probas, dim=-1, keepdim=True)  # (batch, 1)

        # Append sampled index to the running sequence
        idx = torch.cat((idx, idx_next), dim=1)  # (batch, n_tokens+1)

    return idx

选中的代码定义了一个名为 generate_text_simple 的函数,用于生成文本。该函数接受四个参数:

  • model:一个预训练的语言模型,用于生成文本。
  • idx:一个形状为 (batch, n_tokens) 的张量,表示当前上下文中的索引。
  • max_new_tokens:一个整数,表示要生成的最大新令牌数。
  • context_size:一个整数,表示模型支持的上下文大小。

函数的主要功能是通过迭代生成新的令牌,直到达到 max_new_tokens 或满足其他停止条件。具体步骤如下:

  1. 裁剪当前上下文:如果当前上下文的长度超过了模型支持的上下文大小,只保留最后 context_size 个令牌作为上下文。

  2. 获取预测:使用模型对裁剪后的上下文进行预测,得到形状为 (batch, n_tokens, vocab_size) 的对数几率张量。

  3. 聚焦于最后一个时间步:只关注最后一个时间步的对数几率,将张量形状从 (batch, n_tokens, vocab_size) 转换为 (batch, vocab_size)

  4. 应用 softmax 得到概率:对最后一个时间步的对数几率应用 softmax 函数,得到每个词汇表条目的概率。

  5. 获取概率最高的索引:找到概率最高的词汇表条目的索引。

  6. 将采样的索引附加到运行序列:将采样的索引附加到当前上下文中,更新上下文。

  7. 重复步骤 1-6:重复上述步骤,直到生成的令牌数达到 max_new_tokens

  8. 返回生成的索引:返回生成的索引张量。

总结来说,generate_text_simple 函数通过迭代生成新的令牌,逐步扩展上下文,直到达到指定的最大新令牌数。每次迭代中,它使用模型对当前上下文进行预测,并根据预测结果选择下一个令牌。

 

  • The generate_text_simple above implements an iterative process, where it creates one token at a time

Image

  • Let's prepare an input example:
Output length: 10
  • Note that the model is untrained; hence the random output texts above
  • We will train the model in the next chapter

Summary and takeaways

  • See the ./gpt.py script, a self-contained script containing the GPT model we implement in this Jupyter notebook
  • You can find the exercise solutions in ./exercise-solutions.ipynb
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值