P4:构建Makemore第三部分:激活值与梯度,BatchNorm 🧠📈

P4:构建Makemore第三部分:激活值与梯度,BatchNorm 🧠📈在本课程中 我们将从零开始 深入探索神经网络训练的内部机制 我们将从一个空白的 Jupyter 笔记本开始 最终定义并训练一个你自己的神经网络 你将亲眼看到并理解在这个过程中发生的所有事情 Micrograd 是一个自动梯度引擎 Autograd 它实现了反向传播算法 反向传播是一种能够高效计算损失函数相对于神经网络权重梯度的算法 这使得我们能够通过微调权重来最小化损失函数

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



在本课程中,我们将从零开始,深入探索神经网络训练的内部机制。我们将从一个空白的 Jupyter 笔记本开始,最终定义并训练一个你自己的神经网络。你将亲眼看到并理解在这个过程中发生的所有事情。

Micrograd 是一个自动梯度引擎(Autograd),它实现了反向传播算法。反向传播是一种能够高效计算损失函数相对于神经网络权重梯度的算法,这使得我们能够通过微调权重来最小化损失函数,从而提高网络的准确性。它是 PyTorch 或 Jax 等现代深度学习库的数学核心。

Micrograd 的有趣之处在于,它通过构建数学表达式图来工作。虽然它处理的是标量值(出于教学目的),但其背后的数学原理与处理多维张量的生产级库完全相同。

在深入代码之前,我们需要对导数有一个坚实的直观理解。导数衡量的是函数在某个点上的瞬时变化率,或者说斜率。

上一节我们介绍了本课程的目标,本节中我们来看看导数的基本概念。

考虑一个简单的标量函数 。我们可以通过导数的定义来数值近似其在某点 的导数:

 
  

这个公式 在 趋近于 0 时的极限,就是函数在 点的精确导数。它告诉我们,如果我们将 向正方向轻微推动(增加 ),函数值 会如何响应。

对于一个多输入函数,例如 ,我们可以分别计算输出 相对于每个输入 , , 的导数。这些导数(或梯度)告诉我们每个输入对最终输出的影响有多大。

神经网络本质上是复杂的数学表达式。为了处理这些表达式,我们需要一种数据结构来跟踪计算过程。这就是 类的作用。

上一节我们理解了导数的含义,本节中我们开始构建能够表示数学表达式的数据结构。

我们将创建一个 类,它包装一个标量值,并记录该值是如何通过操作从其他值计算而来的。

GPT plus 代充 只需 145

现在,我们需要定义基本的数学运算,如加法和乘法,并让它们返回新的 对象,同时正确设置子节点和操作类型。

 
  

通过这种方式,我们可以构建如 这样的表达式,并形成一个计算图,其中每个 对象都知道自己的来源。

有了表达式图,我们就可以执行反向传播来计算梯度。我们从最终输出开始,逆向遍历整个图。

上一节我们构建了表达式图,本节中我们手动进行反向传播来理解其过程。

以表达式 为例。反向传播的目标是计算损失 相对于所有输入值(如 , , , )的梯度。

  1. 初始化输出梯度:(因为 )。
  2. 通过乘法节点反向传播:对于 ,局部导数是:

    • 根据链式法则,我们将 乘以上述局部导数,得到 和 。

  3. 通过加法节点反向传播:对于 (其中 ),加法节点的局部导数都是 1。因此,梯度直接传递:,。
  4. 通过乘法节点反向传播:对于 ,局部导数是:

    • 同样应用链式法则:,(注意使用 ,因为一个变量可能被多个节点使用)。

链式法则的核心是:若 依赖于 , 依赖于 ,则 。在反向传播中,我们将从输出传来的梯度()与局部梯度()相乘,得到对更早输入的梯度。

手动计算梯度对于复杂网络是不现实的。现在,我们将在 类中实现自动反向传播机制。

上一节我们手动应用了链式法则,本节中我们将这个逻辑编码到每个操作中。

我们为每个 对象添加一个 方法,它定义了如何将该节点的梯度传播到其子节点。

GPT plus 代充 只需 145

为了按正确的顺序调用所有节点的 方法,我们需要对计算图进行拓扑排序。

 
  

现在,调用 将自动填充图中所有 对象的 属性。

一个神经元是神经网络的基本构建块。它接收多个输入,进行加权求和,加上偏置,然后通过一个非线性激活函数(如 tanh)产生输出。

上一节我们实现了自动求导引擎,本节中我们利用它来构建神经网络组件。

一个神经元的数学模型如下:

GPT plus 代充 只需 145

我们可以用已有的 操作来构建它。首先,我们需要实现 函数及其反向传播。

 
  

有了神经元,我们就可以构建层(一组神经元)和最终的多层感知机(MLP),即多个层的堆叠。

神经网络的目的是学习一个映射。我们通过定义损失函数来衡量网络预测与真实目标之间的差距,然后使用梯度下降来最小化这个损失。

上一节我们组装出了神经网络,本节中我们让它学习。

以下是训练一个简单神经网络的关键步骤:

  1. 前向传播:输入数据,计算网络预测和损失值。
    GPT plus 代充 只需 145
  2. 反向传播:计算损失相对于所有网络参数的梯度。
     
  3. 梯度下降更新:沿着梯度的反方向微调参数,以减小损失。
    GPT plus 代充 只需 145
  4. 迭代:重复步骤 1-3 多次。在每次迭代前,需要将参数的 属性归零(),防止梯度累积。

通过不断迭代,网络的预测会逐渐接近目标,损失值会下降。

我们构建的 Micrograd 在概念上与 PyTorch 这样的工业级库是一致的。PyTorch 的核心 就像我们的 对象,它同样有 、 和 方法。主要区别在于 PyTorch 针对效率进行了大量优化,使用多维张量并行计算,并支持 GPU 加速。

在本课程中,我们一起学习了:

  • 导数和梯度的直观意义。
  • 如何构建计算图来表示数学表达式。
  • 反向传播链式法则的原理与实现。
  • 如何从基本的神经元构建多层感知机
  • 如何使用损失函数梯度下降来训练神经网络。

虽然 Micrograd 很简单(约 100 行代码),但它包含了训练现代深度神经网络所需的所有核心概念。其他的一切,都是为了规模和效率。希望这次探索能帮助你揭开神经网络训练的神秘面纱!

在本节课中,我们将学习如何从零开始构建并训练一个 GPT-2 (124M) 模型。我们将涵盖从加载预训练权重、理解模型架构、实现核心模块,到进行高效训练和评估的完整流程。通过本教程,你将能够亲手重现一个功能完整的语言模型。

GPT-2 是 OpenAI 在 2019 年发布的开创性语言模型。它属于一个包含多个尺寸的“迷你系列”,其中 124M 参数版本是一个理想的起点。本节课的目标是理解其架构,并用 PyTorch 从头实现它,最终训练一个性能接近甚至超越原版的开源模型。


首先,让我们明确目标。我们将使用 Hugging Face 库加载 OpenAI 发布的 GPT-2 (124M) 模型,并检查其权重结构。这为我们后续自己构建模型提供了参考基准。

以下是加载模型并检查其权重的步骤:

  1. 导入模型:从 库导入 。
  2. 加载预训练权重:使用 加载 124M 参数版本( 即对应此版本)。
  3. 获取状态字典:模型的 包含了所有权重张量及其形状。
  4. 分析关键参数
    • 标记嵌入 ():形状为 。GPT-2 的词表大小为 50257,每个标记用 768 维向量表示。
    • 位置嵌入 ():形状为 。模型支持的最大序列长度为 1024,每个位置有独立的嵌入向量。
    • Transformer 层权重:包括注意力层、前馈网络层等的权重和偏置。

通过分析这些权重,我们可以确认模型结构,并为后续实现提供准确的参数形状。


上一节我们查看了目标模型的结构,本节中我们来看看如何用 PyTorch 模块搭建我们自己的 GPT-2。

GPT-2 是一个仅解码器的 Transformer 模型。与原始 Transformer 相比,它移除了编码器部分,并调整了层归一化的位置(采用“预归一化”)。

我们首先定义 类作为模型容器。

 
    

每个 包含一个带掩码的多头自注意力机制和一个前馈网络(MLP),均采用预归一化。

GPT plus 代充 只需 145

这是 Transformer 的核心,允许每个标记关注其左侧的所有标记。

 
    

MLP 包含两个线性层,中间使用 GELU 激活函数。GPT-2 使用了 GELU 的近似版本。

GPT plus 代充 只需 145


现在我们已经实现了自己的 GPT-2 类,接下来需要将 Hugging Face 模型中的权重加载到我们的结构中,并验证生成功能是否正常。

以下是关键步骤:

  1. 创建配置对象:确保超参数(层数、头数、嵌入维度等)与 GPT-2 (124M) 一致。
  2. 映射权重名称:遍历 Hugging Face 模型的状态字典,将权重名称映射到我们模型中的对应参数。
  3. 处理权重绑定:注意标记嵌入 () 和语言模型头 () 的权重是共享的。在我们的实现中,可以通过简单地将 指向 来实现。
  4. 验证生成:使用相同的提示词进行文本生成,比较结果以确保模型行为一致。

完成这一步后,我们就有了一个功能上等同于开源 GPT-2 (124M) 的模型,这为我们从头开始训练提供了信心和基准。


上一节我们成功加载了预训练模型,本节中我们来看看如何准备数据并构建训练循环。

我们需要一个能够将文本数据流式转换为模型可接受的批次张量的数据加载器。

 
      

我们使用 AdamW 优化器,并遵循 GPT-3 论文中的设置:使用余弦衰减学习率调度,并带有预热阶段。

GPT plus 代充 只需 145

训练循环的核心步骤包括前向传播、损失计算、反向传播和参数更新。

 
      


为了充分利用现代硬件并加速训练,我们需要应用一系列优化技术。

以下是提升训练速度的关键方法:

  • 降低数值精度:使用 进行混合精度训练(BF16),这能显著减少内存占用并加速计算。
  • 使用 :PyTorch 2.0 引入的编译器可以融合操作,减少 Python 开销和 GPU 内存读写,带来显著的性能提升。
  • 使用 Flash Attention:如果硬件支持,使用 PyTorch 内置的高效注意力实现,它通过算法优化避免了大型注意力矩阵的显式存储和计算。
  • 调整“丑陋”的数字:将词表大小等参数调整为 2 的幂(例如从 50257 调整为 50304),可以使 CUDA 内核运行更高效,因为许多内核是为 2 的幂次方块大小优化的。

为了在多个 GPU 上并行训练,我们使用 PyTorch 的分布式数据并行。

GPT plus 代充 只需 145


我们使用 FineWeb-Edu 数据集(一个高质量的网络文本子集)进行训练,并使用 HellaSwag 基准进行评估。

  1. 下载并预处理 FineWeb-Edu 数据集,将其标记化并保存为分片文件。
  2. 修改数据加载器以支持从多个分片中流式读取数据,并确保在多 GPU 训练中每个进程处理不同的数据块。

HellaSwag 是一个句子补全任务,用于评估模型的常识推理能力。评估方法不是直接让模型选择 A/B/C/D,而是让模型为每个选项计算平均对数似然(或损失),然后选择可能性最高的选项。

 
        

运行上述代码,我们会得到一个名字列表,例如 。这个列表可能按频率排序。让我们进一步查看数据集的一些统计信息:

GPT plus 代充 只需 145

我们预计总词数约为32,000。最短的单词可能只有2个字母,而最长的可能达到15个字符。这些信息对我们构建模型很重要。

上一节我们介绍了数据集,本节中我们来看看如何构建第一个语言模型——双字母模型。在这个模型中,我们只关注连续的两个字符(即“双字母”),并试图用前一个字符来预测后一个字符。这是一个非常简单且基础的模型,但它是很好的起点。

以下是构建双字母模型的关键步骤:

  1. 创建字符到索引的映射:我们需要将字符(如‘a’, ‘b’, …)以及特殊的开始/结束标记转换为整数,以便后续处理。
  2. 统计双字母出现频率:遍历数据集中的所有单词,统计每一个双字母组合出现的次数。
  3. 将频率转换为概率:对统计结果进行归一化,使得给定前一个字符后,所有可能的下一个字符的概率之和为1。

首先,我们创建字符表。除了26个字母,我们引入一个特殊的开始/结束标记‘.’。

 
        

接下来,我们初始化一个计数矩阵 ,其大小为 ,用于统计双字母出现次数。行索引代表第一个字符,列索引代表第二个字符。

GPT plus 代充 只需 145

现在,我们遍历所有单词和其中的双字母进行计数。对于每个单词,我们在其开头和结尾分别加上开始标记‘.’和结束标记‘.’。

 
        

计数完成后,我们可以将计数矩阵 可视化,以直观感受不同双字母组合的出现频率。

为了从计数得到概率,我们需要对矩阵 的每一行进行归一化(即让每一行的元素之和为1)。这里我们使用PyTorch的广播功能高效实现。

GPT plus 代充 只需 145

注意: 参数在这里至关重要,它确保了广播除法的方向正确(按行归一化)。如果设置错误,会导致对列进行归一化,得到完全错误的结果。

现在我们已经有了一个训练好的双字母模型(概率矩阵 )。我们可以利用这个模型来生成新的名字。采样的过程是迭代式的:

  1. 从开始标记‘.’(索引0)开始。
  2. 查看 矩阵中对应当前字符索引的那一行,这是一个概率分布。
  3. 根据这个概率分布,随机抽取下一个字符的索引。
  4. 将新抽到的字符作为当前字符,重复步骤2-3。
  5. 当抽到结束标记‘.’(索引0)时,停止生成。

以下是采样的代码实现:

 
        

运行上述代码,可能会生成如 “mor.”, “axx.” 等名字。虽然这些名字看起来不太像真实名字,但这正是双字母模型能力有限的表现——它只考虑了前一个字符的信息。

为了量化模型的好坏,我们需要一个评估指标。在统计建模中,常用的是似然(Likelihood)和对数似然(Log-Likelihood)。

  • 似然:模型为整个训练集中所有真实出现的双字母分配的概率的乘积。值越高,说明模型对训练数据的拟合越好。
  • 对数似然:由于概率乘积可能是一个非常小的数字,通常取其对数进行处理,称为对数似然。对数是一个单调函数,因此最大化似然等价于最大化对数似然。

然而,在机器学习中,我们习惯最小化一个损失函数。因此,我们定义负对数似然(Negative Log-Likelihood, NLL)作为损失函数。对于我们的双字母模型,损失计算如下:

GPT plus 代充 只需 145

平均负对数似然(即损失)越低,模型质量越好。一个完美的模型(总是预测概率为1)的损失为0。我们还可以在整个训练集上计算这个损失,作为模型的最终评估指标。

模型平滑:在计数时,我们给矩阵 的所有元素加了1()。这个技巧称为“加1平滑”或“拉普拉斯平滑”。它确保了概率矩阵 中没有绝对为零的元素,从而避免了当模型遇到训练集中未出现过的双字母时,对数似然变成负无穷大的情况。

上一节我们通过直接计数和归一化的“统计”方法得到了双字母模型。本节我们将使用神经网络和梯度下降的“学习”方法来达到同样的目的。这虽然对于简单的双字母模型显得大材小用,但这种方法具有极强的扩展性,是构建更复杂模型的基础。

我们的目标不变:输入一个字符(索引),输出下一个字符的概率分布。我们将构建一个极简的神经网络:

  1. 输入层:将字符索引进行独热编码(One-hot Encoding)。例如,索引5变为一个长度为27的向量,只有第5位是1,其余为0。
  2. 线性层:一个没有偏置项(bias)的全连接层。权重矩阵 的形状是 。这相当于用输入向量的索引去“查找” 矩阵的某一行。
  3. Softmax层:将线性层的输出(称为logits)通过指数运算和归一化,转换为概率分布。

首先,我们需要准备训练数据(输入 和标签 )。

 
        

接下来,我们初始化网络参数 ,并实现前向传播过程。

GPT plus 代充 只需 145

现在,我们需要计算损失。损失函数仍然是平均负对数似然。我们需要提取出模型为每个训练样本中真实下一个字符所分配的概率。

 
        

由于 是随机初始化的,初始损失会很高。接下来,我们使用梯度下降来优化 。

GPT plus 代充 只需 145

经过优化,损失会下降到接近我们之前用统计方法得到的值(约2.45)。此时,神经网络的权重矩阵 经过 运算后,就近似等于我们之前通过计数得到的概率矩阵 (的转置)。两种方法殊途同归。

正则化的联系:在统计方法中,我们通过“加1平滑”来防止过拟合。在神经网络方法中,这等价于一种叫做权重衰减L2正则化的技术。我们可以在损失函数中添加一项 ,这会鼓励 的数值变小,从而使输出概率分布更平滑、更均匀,其效果类似于增加平滑计数。

本节课中我们一起学习了:

  1. 字符级语言模型的基本概念:将文本视为字符序列,并预测序列中的下一个字符。
  2. 双字母模型的构建:通过统计字符共现频率并归一化,得到一个简单的概率查找表模型。
  3. 模型评估:使用负对数似然作为损失函数来衡量模型质量。
  4. 神经网络方法:用独热编码、线性层和Softmax构建了等效的模型,并通过梯度下降进行训练。
  5. 两种方法的统一:统计方法和神经网络方法在双字母模型上是等价的,但后者为构建更复杂的模型提供了框架。

双字母模型的能力非常有限,因为它只考虑了一个字符的上下文。在接下来的课程中,我们将扩展这一框架:

  • 考虑更多的前序字符(如3个、5个或整个单词)。
  • 用更复杂的神经网络(如MLP、RNN、Transformer)来代替简单的线性层。
  • 处理更长的序列和更大的数据集。

神经网络方法的强大之处在于其可扩展性。当上下文变长时,可能的组合呈指数级增长,无法再用一个简单的表格来存储所有概率。而神经网络可以通过学习到的参数,泛化到未见过的字符组合上,从而构建出强大的语言模型。

在本节课中,我们将学习如何构建一个多层感知器 (MLP) 模型,用于预测序列中的下一个字符。我们将从回顾上一节课的简单模型开始,然后深入探讨如何通过引入嵌入层和隐藏层来构建一个更强大、更灵活的神经网络模型。

在上一节课中,我们实现了一个基于单个前序字符的简单模型(大词袋模型)。这种方法虽然易于理解,但预测效果不佳,因为它只考虑了一个字符的上下文。当我们尝试增加上下文长度时,可能的组合数量会呈指数级增长,导致模型参数过多且数据稀疏。

为了解决这个问题,本节课我们将转向多层感知器模型。我们将遵循一篇有影响力的论文中的方法,通过将字符嵌入到低维空间,并使用神经网络来捕捉字符间的复杂关系,从而实现对更长上下文的建模。

上一节我们介绍了基于计数的简单模型。本节中我们来看看如何构建一个更复杂的神经网络模型。

简单模型的问题在于其有限的上下文窗口。如果我们只考虑一个字符,模型无法捕捉到更长的依赖关系。考虑两个或三个字符的上下文会使状态空间急剧膨胀,导致模型难以训练。

因此,我们需要一种更高效的方法来建模长距离依赖关系。多层感知器通过引入可学习的嵌入和隐藏层来实现这一点。

我们将参考一篇论文中提出的方法。该方法的核心思想是将每个字符(或单词)表示为一个低维的、可学习的特征向量(嵌入)。

核心概念

  • 嵌入查找表 C:一个矩阵,其行数等于词汇表大小,列数等于嵌入维度。例如, 返回第5个字符的嵌入向量。
  • 前向传播公式:对于上下文中的每个字符索引 ,我们查找其嵌入 。将多个嵌入拼接后,输入到隐藏层:。最后,输出层计算逻辑值:。

在训练开始时,这些嵌入向量是随机初始化的。通过反向传播训练神经网络,语义或功能相似的字符的嵌入向量会在空间中彼此靠近,这使得模型能够泛化到未见过的字符组合。

以下是构建训练数据集的步骤。我们需要将原始的名称列表转换为神经网络可以处理的输入()和标签()对。

我们首先定义块大小(),即用于预测下一个字符的上下文长度。例如,如果 ,则我们使用前3个字符来预测第4个字符。

 
        

这段代码为每个单词生成多个训练样本。 中的每个元素是一个包含 个整数的列表,代表上下文。 中的每个元素是一个整数,代表序列中下一个字符的索引。

上一节我们准备好了数据。本节中我们来看看如何构建神经网络的各个层。

嵌入层 是一个可学习的查找表。它将字符索引映射到一个低维的连续向量空间。

GPT plus 代充 只需 145

对于一个整数索引 ,我们可以通过 直接获取其嵌入向量。PyTorch 支持使用张量进行批量索引,因此对于整个输入批次 (形状为 ),我们可以用一行代码完成所有嵌入查找:,结果形状为 。

隐藏层是一个全连接层,它对拼接后的嵌入向量进行非线性变换。

 
        

为了将形状为 的 输入到线性层,我们需要将其重塑为 。这可以通过 方法高效完成:

GPT plus 代充 只需 145

现在我们可以计算隐藏层激活和输出层的逻辑值。

 
        

是网络对下一个字符的原始预测分数。为了得到概率分布,我们需要对其应用 softmax 函数。

得到逻辑值后,我们需要计算损失以评估模型预测的好坏,并通过反向传播来更新参数。

我们使用交叉熵损失函数。它直接接受逻辑值 和真实标签 (一个包含正确字符索引的张量)。

GPT plus 代充 只需 145

使用 比自己实现 softmax 再计算负对数似然更高效、数值更稳定。它会内部处理可能的数值溢出问题。

训练过程包括前向传播、损失计算、反向传播和参数更新。

 
        

在实际训练中,我们不会使用全部数据(22万个样本)计算梯度,而是采用小批量随机梯度下降,每次迭代只使用一小部分数据,这能极大提高训练速度。

训练模型后,我们需要评估其性能,并调整超参数以获得更好的结果。

为了避免过拟合,我们将数据划分为三部分:

  • 训练集(80%):用于更新模型参数。
  • 验证集(10%):用于调整超参数(如网络大小、学习率)。
  • 测试集(10%):用于最终评估模型性能,应极少使用。

学习率是训练中最重要的超参数之一。一个寻找合适学习率范围的方法是进行学习率扫描。

GPT plus 代充 只需 145

理想的区域是损失快速下降但尚未剧烈震荡的部分。根据扫描结果,我们可以选择一个合理的学习率(例如 0.1)。

如果模型在训练集和验证集上的损失都很高且相近,说明模型可能“欠拟合”,即模型容量不足。我们可以通过以下方式增加容量:

  • 增加嵌入维度(例如从 2 维增加到 10 维)。
  • 增加隐藏层的神经元数量。
  • 增加上下文长度()。

每次调整后,都应在验证集上评估性能,选择效果最好的配置。

训练好的模型可以用来生成新的名称。以下是采样过程:

 
        

本节课中我们一起学习了如何构建一个用于字符级语言建模的多层感知器模型。

我们首先指出了简单计数模型的局限性,然后引入了通过嵌入层将离散字符映射到连续向量空间的思想。我们详细实现了神经网络的前向传播过程,包括嵌入查找、隐藏层变换和输出层计算。我们使用交叉熵损失函数和梯度下降法来训练模型,并讨论了如何通过划分数据集、寻找合适学习率以及调整模型容量(嵌入维度、隐藏层大小)来优化模型性能。最后,我们展示了如何从训练好的模型中采样生成新的名称。

通过本课的学习,你现在应该能够理解并实现一个基本的神经网络语言模型,并掌握对其进行分析和改进的基本方法。

在本节课中,我们将继续构建Makemore项目,深入探讨神经网络训练中的两个核心概念:激活值梯度。我们将分析它们在训练过程中的表现,并学习如何使用批量归一化(Batch Normalization) 这一关键技术来稳定深度神经网络的训练。理解这些内容对于后续学习更复杂的模型(如循环神经网络)至关重要。


上一节我们实现了一个基于多层感知机(MLP)的字符级语言模型。在向更复杂的架构(如RNN、LSTM)迈进之前,我们必须先深入理解神经网络在训练期间内部发生了什么。核心在于观察激活值(Activations) 和反向传播的梯度(Gradients) 的行为。

如果激活值分布不当(例如,过大或过小),或者梯度流动不畅(例如,消失或爆炸),网络将难以有效学习。这对于深度网络尤为关键。本节课,我们将首先诊断并修复初始化问题,然后引入批量归一化层来系统性地控制激活统计量。


神经网络训练的第一步是参数的初始化。糟糕的初始化会导致训练初期出现异常,例如损失值异常高或激活值饱和。

在初始化时,我们希望网络对每个输出字符没有先验偏好,即预测概率应大致均匀。对于27个字符的分类问题,期望的初始损失应为 。

然而,在我们的初始代码中,第一次迭代的损失高达27。这是因为输出层的logits值过于极端,导致softmax输出对错误答案“过度自信”。

问题根源:输出logits由公式 计算得出。初始的 和 值过大。

解决方案:将输出层的权重 和偏置 初始化为较小的值(例如,乘以0.01),使logits接近零,从而让softmax输出接近均匀分布。

GPT plus 代充 只需 145

修复后,初始损失从27降至接近预期的3.29,训练曲线也不再出现初期陡降的“曲棍球棒”形状,这意味着优化过程更有效率。

接下来,我们检查隐藏层的激活值 ,它由 函数产生。理想情况下,我们希望 的输入(预激活值)不要过大,否则 会进入饱和区(输出接近±1),导致梯度消失。

问题诊断:绘制隐藏层预激活值 的直方图,发现其值域很宽(例如-15到15)。这使得 的输出大量集中在±1附近,处于函数的平坦区域。在反向传播时,梯度会乘以 ,当输出接近±1时,这个因子接近零,梯度因此“消失”。

解决方案:同样,通过缩小第一层权重 和偏置 的初始值,来控制预激活值的尺度。

 
          

调整后, 的输入被控制在合理范围(例如-1.5到1.5),输出直方图显示饱和神经元数量大大减少,梯度得以更有效地流动。


上一节我们通过试错法找到了缩放因子(如0.2)。但对于更深的网络,我们需要一个系统性的初始化方法。

核心思想是:我们希望每一层线性变换的输出(在通过非线性函数之前)保持大致相同的分布,通常希望是均值为0、标准差为1的高斯分布。

对于一个线性层 ,假设输入 是标准高斯分布(均值为0,标准差为1)。那么,为了使输出 的标准差也保持为1,权重 的标准差应设置为 ,其中 是该层的输入维度。

然而,当层与层之间插入非线性激活函数(如 或 )时,情况会发生变化。这些函数会压缩或改变输入的分布。因此,我们需要引入一个增益(gain) 因子来补偿。

PyTorch的 初始化方法就内置了针对不同非线性函数的增益计算。

GPT plus 代充 只需 145

通过这种有原则的初始化,我们可以确保深层网络中各层的激活值分布更加稳定,而无需手动调整每个缩放因子。


尽管精心设计的初始化有所帮助,但在训练非常深的神经网络时,维持稳定的激活分布仍然极具挑战。批量归一化(BatchNorm)应运而生,它通过一个可微操作直接对激活值进行标准化,极大地稳定了深度网络的训练。

BatchNorm层的操作发生在线性层之后、非线性激活函数之前。对于一个批次(Batch)的输入 (形状为 ),它执行以下步骤:

  1. 计算批次统计量:计算该批次数据在每个特征维度上的均值()和方差()。
  2. 标准化:使用计算出的均值和方差对批次数据进行标准化,使其均值为0,方差为1。

    ( 是一个很小的数,防止除以零)




  3. 缩放与偏移:引入两个可学习参数 (增益)和 (偏置),对标准化后的数据进行变换。


为什么需要 和 ?
如果只做标准化,网络每一层的表达能力会受到限制(强制为0均值1方差)。 和 允许网络学习最适合当前任务的数据分布形态。

  • 训练模式:使用当前批次的统计量(, )进行标准化。同时,它会以指数移动平均(EMA)的方式更新两个缓冲区(buffer): 和 ,用于估计整个训练集的全局统计量。
  • 推理/评估模式:不再使用当前批次的统计量,而是使用训练阶段估算好的 和 进行标准化。这使得网络可以对单个样本进行前向传播。

 
            

  1. 与偏置项共存:在线性层()或卷积层()后接BatchNorm时,通常应将线性层的 参数设为 。因为BatchNorm中的 已经起到了偏置的作用,原线性层的偏置会在标准化时被减去,变得无用。
  2. 正则化效果:由于标准化时使用的均值和方差来自当前批次,这为每个样本的激活引入了一些噪声(因为批次是随机采样的)。这种噪声起到了轻微的正则化作用,有助于防止过拟合。
  3. 批次大小:BatchNorm的效果依赖于足够大的批次大小(batch size)来获得有意义的统计量估计。批次过小可能导致运行统计量估计不准确,训练不稳定。


为了确保神经网络训练健康,我们需要监控一些关键指标。以下是一些有用的诊断工具:

  • 前向传播激活直方图:绘制每一层激活值(特别是非线性层如 的输出)的直方图。检查是否过度饱和(大量值集中在±1)或过度不活跃(大量值接近0)。
  • 梯度直方图:绘制流经每一层的梯度的直方图。检查梯度是否消失(值非常小)或爆炸(值非常大)。

更重要的指标是参数更新与其数值本身的比率。在一次优化步骤中,参数的更新量为 。我们关心 的尺度。

通常,我们希望这个比率在对数尺度上()大约在 -3 左右(即更新量大约是参数值的千分之一)。这表示训练以稳定、适中的速度进行。

  • 比率远大于-3(如-1):更新过大,可能导致训练不稳定。
  • 比率远小于-3(如-5):更新过小,训练可能过于缓慢。

通过绘制不同网络层参数的这个比率随时间变化的曲线,可以直观判断学习率设置是否合适,以及网络各层是否在协调学习。


本节课我们一起深入探讨了神经网络训练的核心动态:

  1. 初始化的重要性:不恰当的初始化会导致输出层过度自信、隐藏层激活饱和,从而损害梯度流动和训练效率。我们学习了如何通过缩放权重来获得合理的初始激活分布。
  2. 原则性初始化:介绍了基于输入维度()和非线性函数增益(如 的 )的初始化方法(如Kaiming初始化),这为构建更深网络提供了指导。
  3. 批量归一化(BatchNorm):作为稳定深度网络训练的关键技术,BatchNorm通过标准化激活、引入可学习的缩放偏移参数、以及区分的训练/推理模式,极大地缓解了梯度消失/爆炸问题,并带来了正则化益处。
  4. 诊断与监控:我们学习了一套实用的诊断工具,包括检查激活/梯度直方图以及监控参数更新比率,这些工具能帮助我们判断网络训练是否健康,并指导超参数(如学习率)的调整。

通过理解激活、梯度和BatchNorm,我们为接下来探索更强大但也更难以训练的循环神经网络(RNN)架构奠定了坚实的基础。在下一课中,我们将开始构建RNN,并观察这些动态在其中扮演的关键角色。

在本节课中,我们将深入学习如何手动实现神经网络的反向传播。我们将从当前的多层感知器(MLP)架构出发,逐步替换 PyTorch 的自动微分功能,通过手动计算梯度来加深对神经网络内部工作原理的理解。

到目前为止,我们已经实现并训练了一个包含批量归一化层的两层 MLP。我们的神经网络架构如下:

我们取得了不错的损失,并对架构有了较好的理解。然而,当前的代码依赖于 PyTorch 的 来自动计算梯度。本节课的目标是移除这个依赖,手动在张量级别实现反向传播。

理解反向传播的内部机制至关重要,因为它是一个“漏水的抽象”。仅仅堆叠可微函数并期望反向传播自动工作是不够的。如果不理解其内部原理,在调试和优化网络时可能会遇到困难。

历史上,手动编写反向传播曾是标准做法。例如,在 2010 年左右,使用 MATLAB 或 NumPy 手动计算梯度非常普遍。虽然如今自动微分已成为标准,但掌握手动反向传播能让你成为更强大的神经网络实践者和调试者。

本节课的练习将分为四个部分:

  1. 将整个计算图分解为原子操作,并逐一进行反向传播。
  2. 对交叉熵损失进行数学推导,实现其高效的反向传播。
  3. 对批量归一化层进行数学推导,实现其高效的反向传播。
  4. 将所有手动反向传播代码整合,训练完整的 MLP。

现在,让我们开始第一个练习。

在第一个练习中,我们将把前向传播过程分解为许多细小的原子操作(如加法、乘法、指数、对数等),并逐一为每个操作手动计算梯度。我们将从损失开始,逐步反向传播到网络的输入。

以下是实现反向传播所需的关键步骤列表:

  • 计算损失对 logprobs 的梯度 ():损失是正确标签对应 的负平均值。因此,梯度在正确标签位置为 ,其他位置为 0。
    GPT plus 代充 只需 145
  • 计算损失对 probs 的梯度 ():。根据链式法则,。
     
  • 计算损失对计数和计数的梯度:。这里涉及除法和广播,需要小心处理。对于 ,梯度需要沿被广播的维度求和。
    GPT plus 代充 只需 145
  • 计算损失对归一化 logits 的梯度 ():。因此,。
     
  • 计算损失对 logits 和 logit maxes 的梯度:。这里也有广播。
    GPT plus 代充 只需 145
  • 计算损失对 logit maxes 的第二个分支梯度: 来自 的最大值。梯度需要路由到最大值出现的位置。
     
  • 计算损失对隐藏层、权重和偏置的梯度(线性层):。通过维度匹配推导,,,。
    GPT plus 代充 只需 145
  • 计算损失对 tanh 激活前的梯度 ():。导数 。
     
  • 计算损失对批量归一化参数和输出的梯度:。需要处理广播。
    GPT plus 代充 只需 145
  • 继续反向传播通过批量归一化的各个原子步骤:包括对方差、均值、差值等中间变量的梯度计算,过程较为繁琐,需仔细处理广播和求和。
  • 计算损失对第一个线性层参数和输入的梯度:与第二个线性层类似。
     
  • 计算损失对嵌入层参数的梯度 (): 是通过索引从嵌入表 中查得的。梯度需要根据索引累加回 。
    GPT plus 代充 只需 145

通过逐步实现上述所有步骤,我们完成了对整个计算图的原子级反向传播。每一步的梯度都可以与 PyTorch 自动计算的梯度进行比较验证。

上一节我们通过分解原子操作完成了反向传播,但这种方法效率较低。接下来,我们将看到如何通过数学推导来简化关键部分的反向传播。

在第二个练习中,我们将交叉熵损失视为一个整体数学表达式,并通过解析方式直接计算损失对 logits 的梯度,而不是通过多个原子操作。

直观理解是:梯度会降低错误类别(根据网络当前概率)的 logits,并提高正确类别的 logits,推拉的力量是平衡的。

以下是实现代码:

 
              

这个简单的几行代码等价于练习一中从 到 之间所有原子操作的反向传播总和,但效率要高得多。

类似地,对于批量归一化层,我们也可以推导出一个统一的反向传播公式。给定输入 (即 )、输出 (即 )、缩放参数 ()、偏移参数 ()、批次均值 、批次方差 (使用贝塞尔校正 )和小常数 。

经过繁琐但直接的微积分推导(考虑 和 是 的函数,且 相互依赖),我们可以得到损失对输入 的梯度 的解析表达式。

以下是根据推导结果实现的向量化代码:

GPT plus 代充 只需 145

这个公式直接计算了 (即 ),避免了通过所有中间变量反向传播的复杂性。

在最后的练习中,我们将把所有手动实现的反向传播代码片段整合到一起,形成一个完整的、不依赖 的训练循环。

我们将使用练习二和练习三推导出的高效反向传播公式,以及为线性层、激活函数和嵌入层手动编写的梯度计算代码。

关键步骤包括:

  1. 前向传播计算损失。
  2. 手动反向传播计算所有参数的梯度。
  3. 使用计算出的梯度(而非 )更新参数。
  4. 关闭 PyTorch 的梯度跟踪以提升效率 ()。

完成整合后,我们可以运行训练循环。经过足够迭代,损失会下降,并且从模型中进行采样可以生成看起来合理的“名字”,结果与使用自动微分时相同。这证明了我们手动计算梯度的正确性。

本节课中,我们一起深入探讨了神经网络反向传播的内部机制。我们从最基础的原子操作开始,手动计算了每一个步骤的梯度。然后,我们通过数学推导,找到了交叉熵损失和批量归一化层更高效、更简洁的反向传播公式。最后,我们将所有手动代码整合,成功训练了一个多层感知器。

通过这个过程,我们揭开了自动微分的神秘面纱,强化了对梯度如何在网络中流动的理解。这不仅能帮助我们在未来更好地调试神经网络,也让我们对所使用的工具有了更深刻的认识。现在,我们可以充满信心地称自己为“反向传播忍者”了!

在下一节课中,我们将探索更复杂的架构,如循环神经网络(RNN)及其变体(LSTM),以获取更好的模型性能。

在本节课中,我们将继续完善我们最喜爱的字符级语言模型。我们将从之前构建的多层感知器(MLP)架构出发,将其复杂化,以处理更长的输入序列,并构建一个更深、能逐步融合信息的模型。最终,我们将实现一个类似于 WaveNet 的层次化架构。

在之前的课程中,我们构建了一个基于三层感知器的字符级语言模型。它接收三个先前的字符,并尝试预测序列中的第四个字符。本节课的目标是扩展这个模型,使其能够处理更长的上下文(例如8个字符),并通过一个更深的、树状层次化的结构来逐步融合信息,而不是一次性将所有信息压缩到一个隐藏层中。这种架构灵感来源于2016年的 WaveNet 论文,它本质上也是一个自回归模型,用于预测序列中的下一个元素。

我们本节课的起点代码与第三部分结束时的代码非常相似。我们有一个处理好的数据集,包含约18.2万个“给定三个字符预测第四个字符”的示例。我们的代码已经模块化,包含了 、 等层,其API设计模仿了PyTorch的 模块。

首先,我们注意到损失曲线因为批次大小太小而波动剧烈。我们通过将损失列表重塑为二维张量并计算行平均来平滑可视化。

 
              

上一节我们处理了数据可视化问题。本节中,我们将重构模型的前向传播逻辑,使其更加模块化和简洁。

我们之前将嵌入表()和展平()操作放在了层列表之外。为了使结构更统一,我们创建了 和 模块,并将它们加入到层的序列中。

GPT plus 代充 只需 145

接着,我们引入 容器来管理这些层,这进一步简化了代码。 接收一个层列表,并在前向传播中依次调用它们。

 
              

现在,我们的模型定义和前向传播变得非常清晰:

GPT plus 代充 只需 145

上一节我们重构了模型代码。本节中,我们将扩展模型的上下文长度,并引入WaveNet的核心思想——层次化融合。

首先,我们将输入上下文长度从3增加到8。这立即带来了性能提升(验证损失从 ~2.10 降至 ~2.02),因为模型拥有了更多信息。

但是,简单地将8个字符的嵌入向量展平后送入一个线性层,意味着信息被过早地压缩了。我们想要的是像WaveNet那样,以树状结构逐步融合信息:先融合相邻的两个字符,再融合上一层的两个“组”,以此类推。

为了实现这一点,我们需要修改 层。我们创建了一个新的 层,它可以将连续的 个元素拼接在一起,并增加一个“组”的维度。

 
              

然后,我们重新设计模型架构。第一层 将8个字符分成4组,每组2个字符的嵌入被拼接。随后的线性层只处理这“2个字符”的信息。之后,我们再次使用 将4组合并为2组,以此类推,形成一个小型的层次化网络。

GPT plus 代充 只需 145

上一节我们构建了层次化模型。本节中,我们需要修复一个关键问题: 层对多维输入的处理。

我们原来的 实现假设输入是二维的 。但在我们的新架构中, 会产生三维输入 。我们需要让 在训练时,同时计算 和 维度上的均值和方差。

 
              

修复这个Bug后,模型性能得到了小幅但稳定的提升。

通过增加模型容量(如嵌入维度和隐藏层大小),我们最终将验证损失降低到了 1.993 左右,成功跨过了2.0的界限。

本节课我们一起实现了一个简化的WaveNet风格架构。我们学习了如何:

  1. 使用模块化构建块(如 )来组织复杂网络。
  2. 通过 和线性层实现信息的层次化融合。
  3. 调整 以正确处理多维输入。

然而,我们实现的只是WaveNet思想的核心骨架。完整的WaveNet还包括门控激活单元残差连接空洞因果卷积(用于高效计算)。此外,我们缺乏一个系统的超参数搜索和实验框架,目前的优化更多是“猜测与检验”。

在未来的课程中,我们可以:

  • 实现空洞卷积来高效地计算整个输入序列的输出。
  • 添加残差连接以训练更深的网络。
  • 建立实验管线,进行大规模的超参数优化。
  • 探索循环神经网络(RNN/LSTM)和Transformer架构。

挑战:你可以尝试调整本课的模型(如各层通道数、嵌入维度),或者阅读WaveNet论文实现更复杂的层,看看能否击败 1.993 的验证损失记录。


总结:本节课中,我们从基础的MLP出发,逐步构建了一个层次化的、类似WaveNet的字符级语言模型。我们重构了代码使其更清晰,引入了层次化信息融合的概念,并修复了批归一化层的多维处理问题。虽然性能得到了提升,但这仅仅是探索现代深度神经网络架构的开始。

在本节课中,我们将学习如何从零开始构建一个类似 GPT 的 Transformer 语言模型。我们将使用一个简单的字符级数据集(Tiny Shakespeare),并逐步实现模型的核心组件,包括自注意力机制、多头注意力、前馈网络以及残差连接等。通过这个过程,你将深入理解现代大型语言模型(如 ChatGPT)背后的基本原理。


Transformer 架构是当今许多先进 AI 系统的核心,它最初在 2017 年的论文《Attention Is All You Need》中被提出。GPT(Generative Pre-trained Transformer)正是基于此架构构建的。在本教程中,我们将专注于构建一个仅解码器的 Transformer,用于字符级语言建模任务。虽然我们无法复现 ChatGPT 那样的复杂系统,但通过构建一个微型版本,我们可以清晰地理解其工作原理。

我们将从处理数据开始,逐步实现模型的关键部分,并在 Tiny Shakespeare 数据集上进行训练,最终生成莎士比亚风格的文本。


首先,我们需要准备数据并将其转换为模型可以处理的格式。我们将使用 Tiny Shakespeare 数据集,它包含了莎士比亚的所有作品。

我们从指定 URL 下载数据集,并将其读取为一个长字符串。

GPT plus 代充 只需 145

接下来,我们找出数据集中所有独特的字符,构建一个词汇表。每个字符将被映射到一个唯一的整数(标记)。

 
                 

我们将数据集分为训练集(90%)和验证集(10%)。验证集用于评估模型的泛化能力,防止过拟合。

GPT plus 代充 只需 145

由于我们无法一次性将整个数据集输入模型,因此需要从数据中随机抽取小块(批次)进行训练。每个批次包含多个独立的序列,模型将并行处理它们。

以下是创建数据批次的函数:

 
                  

在这个批次中, 是模型的输入, 是每个位置对应的下一个字符的目标值。模型的任务是根据 的上下文预测 。


在深入 Transformer 之前,我们先实现一个最简单的语言模型——Bigram 模型。它仅根据当前字符的身份来预测下一个字符,不考虑任何上下文信息。

Bigram 模型本质上是一个查找表,其中每个字符都直接预测下一个字符的分布。

GPT plus 代充 只需 145

我们可以用简单的优化循环来训练这个模型,并观察其生成效果。

 
                   

Bigram 模型的表现非常有限,因为它没有利用上下文信息。接下来,我们将引入自注意力机制,让字符之间能够进行交流。


自注意力是 Transformer 的核心组件,它允许序列中的每个元素(标记)根据其与序列中其他元素的关系来聚合信息。

自注意力的关键思想是让每个标记生成三个向量:查询(Query)键(Key)值(Value)

  • 查询(Q):表示“我正在寻找什么”。
  • 键(K):表示“我包含什么信息”。
  • 值(V):表示“如果被关注,我将传递什么信息”。

标记之间的亲和力(注意力权重)通过查询和键的点积计算:。然后,我们使用这些权重对值进行加权求和,从而聚合信息。

为了实现语言建模中的因果性(即当前标记不能看到未来标记),我们使用一个下三角掩码矩阵,将未来位置的注意力权重设置为负无穷大,这样在 softmax 后它们的权重就变为 0。

以下是单头自注意力的 PyTorch 实现:

GPT plus 代充 只需 145

在这个实现中:

  1. 我们为键、查询和值定义了线性投影层。
  2. 计算缩放点积注意力分数,并应用因果掩码。
  3. 使用 softmax 将分数转换为概率分布(注意力权重)。
  4. 使用这些权重对值向量进行加权求和,得到输出。


单个注意力头可能只关注特定类型的关系。为了捕捉更丰富的信息,我们并行使用多个注意力头,这就是多头注意力

我们将多个单头注意力的输出在通道维度上拼接起来。

 
                     

在自注意力进行通信之后,每个标记需要独立处理收集到的信息。这是通过一个简单的前馈网络(FFN)实现的,通常是一个两层 MLP。

GPT plus 代充 只需 145

现在,我们将多头注意力和前馈网络组合成一个 Transformer 块。为了优化深度网络,我们引入残差连接层归一化

  • 残差连接:将块的输入直接加到其输出上。这创建了一条梯度高速公路,有助于缓解深度网络中的梯度消失问题。
  • 层归一化:在块内对每个标记的特征进行归一化,稳定训练过程。
 
                     


现在,我们可以将所有组件组合起来,构建完整的 GPT 模型。我们的模型将包括:

  1. 标记嵌入层:将整数标记转换为向量。
  2. 位置嵌入层:为序列中的每个位置提供位置信息。
  3. 多个 Transformer 块(解码器块)。
  4. 最终的层归一化线性投影层,用于预测下一个标记。

GPT plus 代充 只需 145


现在,我们可以使用更大的超参数来训练我们的 GPT 模型,并观察其性能。

 
                       

接下来,实现合并最高频字节对的函数。

GPT plus 代充 只需 145

现在,我们可以编写训练循环,迭代地进行合并,构建词汇表。

 
                       

训练好分词器(获得 和 )后,我们需要实现编码(文本 -> 标记)和解码(标记 -> 文本)功能。

解码相对简单:将每个标记 ID 通过 映射回其字节表示,然后连接并解码为字符串。

GPT plus 代充 只需 145

编码过程需要模拟训练时的合并过程,将文本转换为字节后,反复应用合并规则。

 
                       

我们上面实现的是一个基础的、纯算法的 BPE 分词器。在实际应用中(如 GPT-2, GPT-4),分词器引入了更多规则来处理复杂情况。

预处理规则:例如,GPT-2 使用一个复杂的正则表达式模式,在 BPE 合并之前先将文本分割成不同的块(如字母、数字、标点符号)。这确保了合并只发生在特定类别内部,防止了像将 “dog.” 和 “dog!” 合并成不同标记的情况,使分词更加一致。

特殊标记:除了从数据中学习到的标记,分词器还会引入特殊标记,如 用于分隔文档,或在聊天模型中用于区分用户、助手和系统消息的标记。这些标记在词汇表中拥有独立的 ID,并在处理时被特殊对待。

词汇表大小的影响:词汇表大小是一个关键超参数。太小的词汇表(如字符级)会导致序列过长,消耗大量计算资源。太大的词汇表则会使每个标记出现的频率降低,可能导致嵌入训练不足,同时也会增加模型输出层的计算负担。目前先进的模型通常在数万到十万左右。

需要明确的是,分词器的训练与语言模型本身的训练是两个独立的阶段。

  1. 分词器训练:使用一个代表性数据集(可能与模型训练集不同),运行 BPE 算法,确定合并规则和最终词汇表。这个过程产生 和 两个核心组件。
  2. 模型训练:使用训练好的分词器,将海量的模型训练文本全部转换为标记序列。这些标记序列被保存下来,语言模型在此标记序列上进行训练,学习预测下一个标记。

这种分离意味着我们可以针对不同的目标(如多语言支持、代码处理)优化分词器,而不必重新训练整个大模型。

本节课中我们一起学习了构建 GPT 分词器的核心知识:

  • 分词的重要性:分词是文本进入 LLM 的桥梁,其设计直接影响模型处理各种任务(拼写、多语言、算术、代码)的能力。
  • BPE 算法原理:通过迭代合并最常见字节对来构建词汇表,实现从字符到子词的压缩表示。
  • 分词器实现:我们实现了 、 和 等核心函数,构建了一个可工作的基础分词器。
  • 实际考量:了解了实际分词器(如 OpenAI 的 tiktoken)引入的预处理规则、特殊标记等复杂性,以及词汇表大小等设计选择。
  • 训练流程:明确了分词器训练与语言模型训练是两个独立且先后进行的阶段。

分词虽然是一个预处理步骤,但它深远地影响着语言模型的行为和能力。希望本教程能帮助你揭开分词的神秘面纱,并为深入理解和使用大型语言模型打下坚实基础。

小讯
上一篇 2026-03-13 15:53
下一篇 2026-03-13 15:55

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/216683.html