Skip to content
MisakaTang's Blog
Go back

Let's build GPT: from scratch, in code, spelled out. 视频摘要

Edit page

一、概述(Overview)

本视频是一节由人工智能专家 Andrej Karpathy 主讲的深度实战教学课程。视频的核心论题在于,当今以 ChatGPT 为代表的顶尖大语言模型,其底层架构在技术本质上是一个纯解码器(Decoder-only)的 Transformer 神经网络。视频的核心结论是,通过极其简洁的架构设计和大约几百行原生的 PyTorch 代码,就可以在本地从零开始完整构建、训练并运行一个具备生成莎士比亚风格文本能力的 GPT(Generative Pre-trained Transformer,生成式预训练 Transformer)模型。Andrej Karpathy 强调,虽然现代工业界的超级大模型(如 GPT-3 或 GPT-4)在参数量和数据规模上比视频中的教学模型大出数万到上百万倍,但在数学原理和代码架构上,它们与视频中写出的每一行代码是几乎完全一致的。


二、按照主题来梳理

1. 数据准备与字符级分词机制

在构建 GPT 模型的初始阶段,数据准备和分词(Tokenization)是奠定模型输入基础的第一步。视频中采用了一个小型的文本数据集,即著名的 Tiny Shakespeare(小型的莎士比亚数据集,大小约为 1MB)。这个数据集包含了莎士比亚作品的戏剧对白,整体数据量虽然不大,但足以支撑一个小型 Transformer 模型的拟合与特征学习。

为了让神经网络能够处理文本,我们必须建立一套将离散的文本符号转换为连续的数字向量的映射机制。Andrej Karpathy 在视频中没有直接使用现代工业界复杂的 Subword(子词分词,如 BPE 算法),而是回归到了最本质、最易于理解的 Character-level Tokenization(字符级分词)。他首先通过 Python 的 set 构造函数提取出数据集中所有出现过的去重字符,并将其转换为一个有序的列表。在 Tiny Shakespeare 数据集中,这些字符总共包含 65 个(包括大小写字母、标点符号、空格以及换行符)。这 65 就是该模型的 Vocabulary Size(词表大小)。

基于这个固定词表,视频中手写了两个基础的映射字典:一个是 stoi(String to Integer,字符串到整数的映射),另一个是 itos(Integer to String,整数到字符串的映射)。这两个字典能够将词表中的每一个字符赋予一个从 0 到 64 的唯一整数编码。有了这两套字典后,文本的预处理就可以通过两个核心函数来实现:encode(编码函数)接收一段纯文本字符串,将其逐字替换为对应的整数序列;decode(解码函数)则接收一个整数列表,将其重新还原为人类可读的文本字符串。

字符级分词机制的优势在于其极致的简洁性,模型的词表非常小,不会出现未登录词(Out-of-Vocabulary)的现象。然而,Karpathy 在括号中也特别补充注释指出,字符级分词虽然简化了编码实现,但其代价是牺牲了单个 Token(特征标记)所能承载的信息密度。在现代大规模语言模型(如 GPT-4 或 Google 发布的各大模型)中,工业界普遍使用更为高级的分词器(比如基于 Byte-Pair Encoding,即字节对编码算法的分词器),其词表大小通常会达到 32,000 甚至 50,000 以上。现代分词器能够将诸如 “Google” 这样的高频单词识别为一个单独的 Token,而视频中的字符级分词则需要将其切分为 6 个独立的字符 Token。这意味着字符级分词在处理相同长度的文本时,会拉长其序列长度,增加了模型的长距离建模负担。

2. 数据集切分与批处理数据流设计

在完成了整体文本的整数编码后,整个 Tiny Shakespeare 数据集被转换成了一个巨大的一维张量(Tensor),其类型为 PyTorch 的 torch.tensor。为了在后续对模型的泛化能力进行客观公正的评估,必须对数据进行合理的划分。视频中采取了 90% 与 10% 的切分原则:前 90% 的编码数据被单独抽离出来作为 Training Set(训练集),用于模型参数的梯度更新;后 10% 的数据则作为 Validation Set(验证集),专门用于在训练期间监控模型是否发生了 Overfitting(过拟合)。

为了将这些连续的数字流送入 Transformer 模型进行并行化训练,我们需要引入两个极为关键的心智超参数:block_size(块大小,在某些文献中也被称为上下文长度 context length)和 batch_size(批次大小)。在视频的教学演示中,block_size 被初始化为 8,而 batch_size 被初始化为 4。

理解 block_size 的底层逻辑对于掌握自回归语言模型(Autoregressive Language Model)至关重要。当 block_size 为 8 时,意味着模型在进行预测时,最多只能同时观测到前面 8 个字符。但这并不意味着一个长度为 9 的数据块只包含一个训练样本。相反,在自回归模型的设计中,一个长度为 9 的文本切片里实际上蕴含着 8 个独立的隐式训练样本。举例来说,假设一个文本切片为 [18, 47, 56, 57, 58, 1, 15, 47, 56],模型在内部训练时会同时并行学习以下 8 个前置序列到后续目标的映射:

通过这种巧妙的设计,模型在不同的位置上同时训练了处理长度从 1 到 block_size 不等的各种上下文的能力。为了提高计算设备(如 GPU)的硬件吞吐量和并行计算效率,我们不能一次只处理一个这样的长条样本。因此,需要引入 batch_size。在每个训练步骤中,代码会利用随机数生成器在长文本张量中随机抽取 batch_size 个起始索引,然后各自向后截取长度为 block_size 的片段。这些片段被堆叠在一起,最终形成一个形状为 (batch_size, block_size) 的二维输入张量 ,以及一个形状完全相同但整体向右平移了一格的二维目标张量 。这个 张量正是模型在每一个对应位置上应该预测出的正确下一个字符标签。

3. 从 Bigram Baseline 走向基础语言模型框架

在正式引入复杂的 Transformer 架构之前,Andrej Karpathy 遵循了软件工程和算法工程中的最佳实践:先构建一个最简单的 Baseline(基线模型)。在这个视频中,这个基线模型就是传统的 Bigram Language Model(二元语法语言模型)。

二元语法模型的底层假设非常直白:它认为当前位置预测下一个字符的概率,仅仅取决于当前这一个字符本身,而与当前字符之前更遥远的历史上下文完全无关。在 PyTorch 中,这个 Bigram 模型被定义为一个继承自 nn.Module 的简单类。其核心参数只有一个嵌入矩阵(Embedding Table),其实质是一个形状为 (vocab_size, vocab_size) 的二维权重矩阵。

当输入一个形状为 (B, T) 的张量时(其中 代表批次大小, 代表时间或上下文长度),模型会直接通过 nn.Embedding 查找每一行输入标识符对应的概率分布对数。查找出来的结果形状为 (B, T, C),在这个初级模型中,这里的通道数 (Channels)的大小正好等于词表大小 vocab_size。这里的输出实际上就是所谓的 Logits(未归一化的概率对数)。

为了衡量这个基线模型的预测好坏,我们需要计算损失函数。在分类预测任务中,标准工具是 Cross Entropy Loss(交叉熵损失函数)。然而,PyTorch 的 F.cross_entropy 函数对输入的维度有着严格的要求。它期望接收一个二维的 Logits,形状为 (N, C),其中 是样本总数,而目标张量则应该是一个一维的、形状为 (N,) 的标签。为此,Karpathy 在代码中演示了关键的张量维度重构(Reshape)操作。具体来说,通过调用 logits.view(B*T, C) 将三维的 Logits 展平为二维,同时通过 targets.view(B*T) 将目标张量展平为一维。只有经过这样重构之后,交叉熵损失才能被正确计算。

通过简单的微积分原理,我们可以得出一个极具指导意义的数学常识:由于词表大小为 65,如果一个模型完全处于随机瞎猜的初始状态,那么它在每个位置猜对字符的概率就是 。对 取负对数(),可以算出理论上的初始交叉熵损失应当约为 4.17。如果在模型刚刚初始化、还没开始训练时,算出来的 Loss 远远偏离 4.17,那就说明代码的维度转换或损失计算逻辑出现了严重的 Bug(程序错误)。

尽管 Bigram 模型可以通过 PyTorch 的 AdamW 优化器进行多轮梯度迭代训练,使其 Loss 从 4.17 降低到 2.5 左右,但由于它缺乏对长距离历史上下文的理解能力,它所自回归生成的文本依然是一堆完全无法读懂、甚至无法拼成正常单词的随机字母组合。这自然而然地引出了引入更强大架构——Transformer 的迫切需求。

4. 自注意力机制的数学演进与物理含义

为了克服 Bigram 模型无法利用长上下文的致命缺陷,视频进入了整堂课程最核心的理论高地:Self-Attention Mechanism(自注意力机制)的推导与实现。

Andrej Karpathy 首先提出了一个直观的物理问题:如果我们希望让当前位置的 Token 融合之前所有出现过的历史 Token 的信息,最朴素的数学手段是什么?答案是求平均。也就是说,把从第 0 个位置到当前第 个位置的所有特征向量加起来,然后除以 。这种方法在视频中被称为 “基于数学求和的历史融合”。

为了在 PyTorch 中高效、并行地实现这种对历史信息的渐进式累加平均,Karpathy 展示了三种不断递进的代码编写方案:

在确立了基于掩码和 Softmax 的计算框架后,Karpathy 正式揭示了自注意力机制中三种不同物理含义的向量:Query(查询向量)、Key(键向量)和 Value(值向量)。

在具体的 PyTorch 代码中,我们通过三个独立的线性映射层(nn.Linear)将输入的特征张量分别投影为 。接着,通过计算 的转置的矩阵乘法,得到一个名为 wei(Weights,原始注意力权重)的张量:

这个矩阵乘法的物理本质是在计算不同位置之间的相关性评分(Affinity Scores)。第 行第 列的值,代表了第 个位置的查询对第 个位置的键的匹配程度。在算完这个相关性得分后,紧接着进行上述的 -inf 掩码填充,以确保未来的 Token 信息绝对不会泄露给当前位置。随后通过 Softmax 归一化,得到真正的注意力权重分布矩阵。最后,用这个权重矩阵去乘真正的 Value(值向量)张量。这样一来,每一个位置输出的特征,就不再是盲目的算术平均,而是根据语义相关性进行加权聚合的高级特征表示。

5. 缩放点积注意力与多头注意力架构

在手写完基础的自注意力计算流后,Andrej Karpathy 进一步剖析了 Transformer 论文中一个看似不起眼但极其关键的数学设计:Scaled Dot-Product Attention(缩放点积注意力)中的”缩放”操作。

在论文的公式中,Query 和 Key 在做完点积之后,必须除以一个缩放因子 (其中 代表 Head Dimension,即单个注意力头的维度大小)。Karpathy 在高亮的代码段中专门解释了这一步的数学机理。假设输入的 Query 和 Key 向量各自都符合标准正态分布(即均值为 0,方差为 1),那么当它们进行维度为 的点积运算时,根据概率论的独立随机变量相加性质,点积结果的均值依然是 0,但其方差会急剧扩大到

如果直接把一个方差非常巨大、数值分布极度发散的张量送进 Softmax 函数,会导致一个灾难性的后果:Softmax 会被完全”饱和”(Saturated)。也就是说,由于输入值之间差异过大,经过指数化后,最大的一部分数值会牢牢占据几乎接近 1 的概率,而其他绝大多数位置的概率会被压制到接近 0。在神经网络的反向传播过程中,由于 Softmax 处于饱和状态,其对应位置的局部梯度(Gradient)会变得极其微小,甚至直接趋近于 0。这就会引发深度学习中经典的 Gradient Vanishing(梯度消失)问题,导致模型完全无法通过随机梯度下降来更新前面的参数。通过除以 ,可以将点积结果的方差重新拉回到 1 左右,从而确保 Softmax 内部的数值分布相对平缓,梯度能够畅通无阻地向前传导。

为了进一步增强模型的表征能力,模型不能只从一个视角去看待上下文。因此,我们需要构建 Multi-Head Attention(多头注意力机制)。多头注意力的核心思想是,允许模型同时运行多个独立的自注意力计算过程。例如,我们可以设置 4 个独立的注意力头(num_heads=4),每个头都在一个较小的特征空间里独立计算自己的 Query、Key 和 Value。

在代码实现上,Karpathy 首先编写了一个单独的 Head 类来处理单头的注意力计算。随后,他编写了一个名为 MultiHeadAttention 的类。在这个类的内部,通过 nn.ModuleList 实例化了 4 个 Head 对象。在前向传播过程中,输入特征会同时被送入这 4 个头中分别进行计算,然后将 4 个头输出的张量在最后一个维度上通过 torch.cat 拼接(Concatenate)起来。最后,再用一个线性投影层(Projection Layer)对拼接后的整体特征进行一次混合线性变换。这种设计可以让不同的注意力头各司其职,比如某个头专门负责关注前文中的代词指代,另一个头专门负责寻找句式中的动词关联,从而极大地丰富了模型的空间感知能力。

6. 解码器 Block 的完整组装与优化稳定技术

当多头注意力机制构建完毕后,我们依然不能直接通过疯狂堆叠注意力层来构建深层网络。因为纯粹的注意力层只是在做空间上的特征”聚合”与信息交互,它本身严重缺乏对融合后特征进行深度非线性变换与加工的能力。因此,在每一个完整的 Transformer Block(Transformer 块)内部,紧跟在多头注意力层后面的必须是一个独立的 Feed-Forward Network(FFN,前馈神经网络层)。

在前向传播代码中,这个 FFN 被实现为一个简单的连续小网络:它包含一个线性层,将特征维度放大到原本的 4 倍(按照 Attention Is All You Need 论文中的标准设计);紧接着是一个激活函数层,视频中使用了 nn.ReLU(在现代 GPT 中通常使用 GeLU,但 Karpathy 为了保持代码极端纯粹,在初级阶段使用了 ReLU);然后是一个将维度缩回原样的线性层。这个前馈网络是独立作用于每一个时间步(Token)上的,它的核心职责在于让每个位置的特征在融合完上下文后,进行充分的自我消化与非线性特征提取。

随着网络层数的不断加深(例如将 Block 堆叠到 6 层甚至更高),深层网络普遍会面临两个巨大的物理惩罚:梯度消失和梯度爆炸。为了让上百层的神经网络能够稳定收敛,必须引入两项革命性的工程优化技术:Residual Connections(残差连接,或称 Skip Connections)和 Layer Normalization(层归一化)。

残差连接的数学形式极其优雅:。在代码中,多头注意力和前馈网络层的输出都会被直接加回到它们的输入张量上。这种”直通公路式”的设计允许来自损失函数的梯度在反向传播时,可以毫无阻碍地沿着这条加法通路直接流回最底层的嵌入层,极大地缓解了深层网络训练难的问题。

而 Layer Normalization(层归一化)则负责在训练过程中动态保持数据各维度的统计稳定性。Karpathy 在视频中特意手写了一段小代码来演示 LayerNorm 与 BatchNorm(批归一化)的本质区别:BatchNorm 是对单个特征维度在整个 Batch(批次)的所有样本上求均值和方差;而 LayerNorm 则是对单个样本(即单个 Token 向量)内部的所有通道维度求均值和方差。这意味着 LayerNorm 的操作完全独立于 Batch 之间的干扰,非常适合处理序列长度高度可变的文本数据。

此外,视频中采用的是现代工业界更为推崇的 Pre-LN(前置层归一化)架构。也就是说,在数据进入多头注意力和前馈网络之前,先对其进行 LayerNorm 归一化,然后再进行层计算,最后与残差边相加。这与 2017 年原始 Transformer 论文中的 Post-LN(后置层归一化)有所不同,Pre-LN 已经被无数实验证明能够在训练初始阶段提供高得多的数值稳定度。为了防止模型在 Tiny Shakespeare 这种小数据集上产生极其严重的过拟合,Karpathy 还在每个残差层以及注意力权重后面细心地插入了 nn.Dropout(丢弃法)层,通过随机失活一部分神经元来强迫模型学习更具鲁棒性的特征。

7. 模型的自回归生成与预训练/微调全景图

当所有的 Transformer Block、Token 嵌入层(nn.Embedding)以及用于感知绝对位置信息的 Position Embedding(位置嵌入层)被完美组装进一个庞大的 GPTLanguageModel 类中后,模型便具备了前向推理和计算 Loss 的全部功能。然而,训练好的语言模型最终是要拿来产出文本的,这就需要引入 Autoregressive Generation(自回归生成算法)。

GPTLanguageModel 内部,Karpathy 编写了一个名为 generate 的核心成员函数。这个函数接收一个初始的上下文张量,并指定希望新生成的 Token 数量。自回归生成的精妙步骤在代码中展现得淋漓尽致:

  1. 步骤一: 考虑到模型在设计时有严格的 block_size 上下文限制,如果当前输入的总长度超过了 block_size,必须使用 Python 的切片操作 idx[:, -self.block_size:] 截取最后最新的 block_size 个 Token。因为模型内部的位置编码矩阵无法接收超出其设计范围的索引。
  2. 步骤二: 将截取后的输入送入模型,运行完整的前向传播,拿到当前时间步在整个词表上的 Logits。
  3. 步骤三: 重点关注 Logits 张量在时间维度上的最后一个位置(即 位置)。这个位置对应的概率分布,正是模型对”下一个字符”的最新预测。
  4. 步骤四: 对这个最新的 Logits 调用 F.softmax,将其转化为和为 1 的概率概率分布张量。
  5. 步骤五: 不采取贪婪策略(即不直接取概率最大的那个),而是使用 torch.multinomial 函数,根据算出的概率分布进行概率采样。这种随机采样赋予了模型某种创造力,也是为什么输入相同提示词模型会给出不同结果的原因。
  6. 步骤六: 将新采样出来的字符整数通过 torch.cat 追加到原始输入张量的末尾,从而更新上下文。接着进入下一个循环,周而复始。

在视频的尾声部分,Andrej Karpathy 站在更高的高度,为所有观众梳理了从他手写的这个纳米级 nanoGPT 走向真正如日中天的 ChatGPT 的全景式发展路径。他指出,他们在这堂课上完整编写并实现的过程,在工业界被称为 Pre-training Stage(预训练阶段)。预训练的核心目的是为了得到一个强大的 Base Model(基线大模型)。这个模型在本质上扮演的角色只是一个极其擅长根据前文进行续写的”Document Completer”(文档补全器)。如果你给它输入一个问题”如何修理自行车?“,基线大模型可能不会回答你,而是倾向于为你续写出另一个类似论坛帖子里的新问题。

为了将一个只会机械补全大块网页文本的基线模型,改造成能够像人类专家一样温柔听话、对答如流的智能对话助手(如 ChatGPT),必须在预训练的基础之上,继续进行极其复杂的 Aligning / Fine-tuning Stage(对齐与微调阶段)。这一后续阶段在工业界通常包含以下三大核心硬核步骤:

  1. SFT(Supervised Fine-Tuning,监督微调): 人工撰写海量的高质量”提示词-完美回答”对。强制让模型去拟合这些特定的问答格式,使其从文档续写器转变为形式上的问答机器。
  2. 训练奖励模型(Reward Model): 让模型针对一个提示词给出多个不同的候选回答,由人类标注员(Human Raters)对这些回答的好坏进行 Preference Ranking(偏好排序)。利用这些排序数据去训练另一个独立的神经网络,使其学会像人类一样去对AI的回答打分。
  3. RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习): 利用刚才训练好的奖励模型作为环境的反馈,采用 PPO(Proximal Policy Optimization,近端策略优化算法)这种复杂的策略梯度强化学习算法,去反复迭代微调大模型的采样策略。这能确保模型最终生成的每一个字,都能获得奖励模型最高的分数。

Karpathy 坦言,微调和对齐阶段需要耗费大量非公开的内部人工标注数据,这在开源社区通常是极难完美复制的。而通过 nanoGPT 深入理解预训练阶段的每一行矩阵乘法,才是真正揭开人工智能神秘面纱的最底层心智钥匙。


三、框架与心智模型(Framework & Mindset)

1. 探索性张量维度对齐与调试心智模型(Exploratory Tensor Alignment Framework)

在利用 PyTorch 等现代深度学习框架构建任何复杂的神经网络(如 Transformer)时,AI 研究员和软件工程师面临的最大心智挑战通常并非数学公式本身的艰深,而是海量多维张量在经历各类矩阵变换、切片、拼接以及视角重构时所发生的维度冲突。Andrej Karpathy 在视频中展现出了一套极为成熟的”探索性张量维度对齐与调试心智模型”。这套模型可以被抽象并重写为以下条理清晰的工程实践步骤:

这套心智模型的底层核心在于,将高维、抽象且不可视的张量运算流,通过”预设维度注释-单步打印验证-极端数学理论值对齐”的三段式严密逻辑链条进行锚定。它能帮助开发者在面对复杂的自注意力多头拼接时,始终保持对底层硬件显存中数据排列形态的极致掌控,是避免写出带有隐式维度 Bug 的最高效的系统化方法。

2. 向量交互语义矩阵化转换心智模型(Vector Affinity Matricization Mindset)

在处理序列数据(如文本、音频或时间序列)时,传统程序员往往会陷入一种自发的”序列循环(Sequential Looping)“心智陷阱,即习惯性地使用 for 循环去逐个处理每个时间步。然而,现代深度学习的算力核心(GPU)在本质上是一个极为恐怖的矩阵并行乘法加速器。Karpathy 在解释自注意力机制时,成功展示了如何将一个抽象的”时间步信息融合问题”一步步升华为”高度并行化的语义矩阵化转换心智模型”。其重写后的系统化步骤如下:

这套心智模型彻底颠覆了传统的流式数据处理观。它告诉我们,无论是自回归的因果限制,还是错综复杂的语义相关性,在高度发达的深度学习框架眼里,最终都可以而且必须被合并、打包并升华为一个统一的、可以在 GPU 架构中瞬间完成吞吐的庞大矩阵乘法公式。掌握了这一思维框架,才能在设计新的神经网络架构时,自然而然地做到既符合数学逻辑,又对硬件性能极度友好。


Edit page
Share this post on:

Previous Post
Deep Dive into LLMs like ChatGPT 视频总结
Next Post
自托管服务管理:我的 docker-compose 归集项目