# 纯 C 语言从零开始构建 GPT(逐步解析)
**作者**: vixhaℓ
**日期**: 2026-02-14T18:05:59.000Z
**来源**: [https://x.com/TheVixhal/status/2022734079167467711](https://x.com/TheVixhal/status/2022734079167467711)
---

在本文中,我们将完全从零开始构建一个可用的 GPT 模型,使用纯 C 语言,没有外部库,没有依赖,只有纯粹的计算能力。
先修条件:
- C 编程基础知识
- 理解 GPT 架构
- 对线性代数和微积分的熟悉
让我们一起创造一些非凡的东西。
第一部分:随机数生成
在训练任何神经网络之前,我们需要权重初始化和采样的随机性。Python 有随机程序,但我们用的是 C,所以我们会自己实现。
## 理解伪随机数生成
我们将使用 xorshift 算法, 这是一种快速、简单的 PRNG,非常适合我们的需求。该算法保持内部状态,并通过异或和位移作将其转换,生成看似随机的数字。
发生什么事了? 这三个异或运算以一种具有良好统计性质的序列方式对比特进行扰乱。换位次数(13、7、17)经过精心选择,以最大化重复前的间隔。
## 均匀随机数
现在我们将随机整数转换为0到1之间的浮点数:
为什么是 53 比特? 那是双精度浮子的精度。我们向右移动 11 以使用前 53 位(64 - 11 = 53)。
## 高斯随机数
神经网络需要正态分布的随机值来初始化权重。我们将使用 Box-Muller 变换将均匀随机数转换为高斯随机数:
数学原理:Box-Muller 取两个均匀随机变量 U₁ 和 U₂,并对它们进行变换:
- Z = √(-2 ln U₁) × cos(2π U₂)
这个 Z 分布遵循标准正态分布!按标准值(std,以平均值平移(Shift 来得到任意高斯。
## 洗牌阵列
训练时,我们需要重新调整数据集:
这就是费舍尔-耶茨的洗牌, 保证了每种排列的可能性都一样。
第二部分:数据加载与令牌化
GPT 需要文本数据来学习。我们会加载文档,构建一个角色级分词器。
为什么要采用这种结构? 我们把每一行都作为单独的文档存储。这让我们可以把每行都当作独立的训练序列,非常适合学习短文本中的模式(比如名字或短语)。
## 构建一个角色级代币管理器
我们不再用基于单词的标记,而是用字符。这样更简单,也适用于小型模型:
为什么是钢铁兄弟会?起始令牌告诉模型“从这里开始生成”。这就像一个启动生成的提示。
为什么要排序?不是绝对必要,但当角色在多次运行中有一致的 ID 时,调试会更容易。
第三部分:模型架构 - 超参数
现在我们定义 GPT 的结构。以下是训练速度极小模型的小型数值:
了解尺寸:
- N_EMBD (64):每个标记以 64 维向量表示。这与 GPT-3 的 12,288 个相比微不足道,但足以学习简单的模式。
- N_HEAD(4): 多头注意力将嵌入分为 4 个 4 维头。每个头部可以处理不同的图案。
- N_LAYER(2):我们只用两个变压器模块。真实的 GPT 模型会堆叠几十个这样的。
- BLOCK_SIZE (32):我们同时查看 32 个上下文符号。GPT-3 使用 2048。
- MLP_DIM (256): 前馈网络嵌入维数为 4×,遵循标准变压器架构。
第四部分:参数与梯度的分配
神经网络只是可学习数字(参数)的集合。我们需要存储空间:
参数本身
它们的梯度(用于反向传播)
优化器状态(针对亚当)
## 参数结构
为什么有这么多阵? 模型中的每个变换都需要自己的权重矩阵。d_前缀表示“导数”(梯度),adam_m/adam_v 表示基于动量的优化。
## 初始化函数
为什么要做高斯初始化? 随机初始化会破坏对称性。如果所有权重都从零开始,所有神经元都会学到同样的东西。选择标准差(通常为 0.02)是为了保持激活在一个合理的区间。
## 初始化所有参数
为什么要将输出投影初始化为 0? 这是 GPT-2 的一个技巧,称为“零初始化”,用于残留连接。这让训练在早期更稳定。
第5部分:反向传播的激活存储
在前传过程中,我们需要保存中间值。在反向传递过程中,我们会用这些数据计算梯度。
为什么要保存这一切? 反向传播是正向传递的反向。要计算每个参数对损耗的贡献,我们需要知道前向传递过程中流过的数值。
## KV 缓存与梯度累加器
什么是 KV 缓存? 在注意力中,每个位置查询所有之前的位置。我们会存储所有位置的键和值投影,这样就不会重新计算它们。这对于高效的自回归生成至关重要。
第六部分:构建模块——前传作
现在我们实现基本作:线性层、归一化和 softmax。
## 线性变换(矩阵乘法)
数学形式: 这是 y = Wx,其中 W 是矩阵,x 是向量。每个输出元素是 W 与 x 的一行的点积。
为什么要限制? 该关键词告诉编译器指针不会有别名(重叠),从而实现更好的优化。
为什么是内线? 这些函数被调用多次;内联化避免了函数调用的开销。
## 均方根归一化
我们用 RMSNorm(均方根归一化)代替 LayerNorm,这种方法更简单,效果同样好:
数学方法:RMSNorm 计算:
为什么要正常化? 它能将激活保持在稳定范围内,防止爆炸或消失的梯度。1e-5 防止除以零。
为什么要退缩? 我们需要在反向传播时使用它来正确计算梯度。
## Softmax
Softmax 将日志转换为概率:
数学原理:
为什么要减去最大值? 没有它,exp(x)可能会溢出较大的 x。减去最大值会将所有值移到安全区间。从数学上讲,这并不改变结果:
第七部分:后向传球构建模块
反向传播需要我们前向作的导数。让我们去实施它们吧。
## 线性层反向
线性前进为 y = Wx。向后计算的梯度分别是 x 和 W:
计算方法:
给定 y = Wx 和损失 L:
- ∂L/∂x = W^T (∂L/∂y) ->链式规则,带移位 W
- ∂L/∂W = (∂L/∂y) x^T -> 外积
为什么要累积+=? 多次运算的梯度相加(链式规则)。
## RMS 归一化反向
这是最棘手的导数。前锋是:
## 倒退的:
数学原理: 这来自应用于 RMSNorm 的链式规则。第二项解释了变习如何影响整个向量的有效值,进而影响所有输出。
推导概要:
第八部分:前传——GPT 的实际行动
现在我们把所有东西组装成前传。这里才是魔法发生的地方。
## 从嵌入开始
发生什么事了?
- WTE[token_id] 查找该标记学习到的嵌入向量
- WPE[pos_id] 增加了位置信息(令牌 0 与令牌 5 的编码不同)
- 这个和就是我们的初始表示
为什么要加位置? 注意力没有秩序的概念。位置嵌入注入“这是第三个令牌”的信息。
## 初始归一化
现代变压器对输入进行归一化以保证稳定性。我们会在原地进行归一化(x 既是输入也是输出)。
## 变压器层
现在说说 GPT 的核心,变压器块。我们先走一层(我们只有一层,但代码支持多层):
残留连接: 我们在修改之前保存 x。稍后我们会重新添加(跳过连接)。
## 多头自我关注:查询、键、值投射
QKV 变换:
- 问题(Q): 我在寻找什么?
- 关键(K): 我掌握了什么信息?
- 价值(V): 我应该发送哪些信息?
每个代币都能获得这三个。然后我们通过比较与键的查询来计算注意力。
## 计算注意力权重
注意力计算:
为什么要按√head_dim 来扩展? 没有缩放,点积会随着维数增长,使得软极大(softmax)过于尖锐。缩放保持变异在 1。
## Softmax 与注意力输出
现在 al[tt] 包含概率:“我应该关注多少位置 tt?”
## 加权价值和
刚才发生了什么? 每个头计算:
这是一个加权平均,权重是“位置 t 有多重要?”
## 输出投影与残差连接
残差联系: x = f(x) + x,而非 x = f(x)。 这形成了坡度高速公路,使得深度网络可训练。
## MLP(前馈网络)
关注之后,是一个简单的前馈网络:
ReLU 平方: 如果 x > 0 否则 0 我们用
## 最终 MLP 预测与剩余
现在我们已经完成了变压器层。向量 x 包含处理后的表示。
## 语言模型首脑
最后,我们将预测到词汇大小,以获得每个可能下一个标记的 logit:
Logits: 这些是未归一化的分数。高 logit = 模型认为该代币很可能是下一个。
第九部分:倒向传递——反向传播
这里是我们计算梯度的地方。我们向后游历计算图,累计每个参数的∂损失/∂参数。
## 概述与设置
我们处理局面顺序是逆向的(反向传递是......倒着)。
## 从失利开始
交叉熵导数: 对于 softmax + 交叉熵 ,梯度简单为:
这是一个非常漂亮的简化!softmax(x) 与 cross_entropy就是 softmax(x) - target_one_hot。
## 语言模型 Head Back
我们计算:
- 诊断: 改变最终隐藏状态如何影响流失
- d_lm_head: 语言模型权重的变化如何影响减重
## MLP 倒退
现在我们反向穿过每一层:
ReLU 导数平方:
我们通过链式规则乘以上方梯度 (d_h2[i])。
残差连接梯度: 当正向 y = f(x) + x 时 ,向后 dx + = dy。梯度同时流经函数和跳跃连接。
## 注意力向后(难点)
反向注意很复杂,因为注意力将所有姿势联系在一起:
发生什么事了? 前锋是:
倒着:
## Softmax 反向
Softmax 雅可比矩阵:softmax 的导数为:
其中 δij 为 1,若 i=j,否则为 0。这导致了:
## 查询与密钥梯度
点积导数: 如果 z = x · y,则:
而且我们会按比例相乘 ,因为前进是这样。
## QKV 向后投影
这三种投影都对归一化输入 d_xn 的梯度有所贡献 。
## 完成图层
## 嵌入梯度
最后,梯度会回流到嵌入:
为什么要积累? 多个位置可能使用同一个令牌,所以我们将它们的所有梯度加在一起。
第 10 部分:优化 - Adam Optimizer
现在我们有了梯度。我们需要更新参数。我们将使用 Adam(自适应力矩估计),这比普通的 SGD 好得多。
## 亚当更新步骤
亚当直觉:
- m(动量):度的指数移动平均。有助于在稳定方向加速。
- v(方差):方梯度的指数移动平均。根据参数调整学习速率。
- 偏置纠正: 训练初期,m 和 v 偏向 0。我们会纠正这个问题。
更新规则:
为什么有效: 参数在大且均匀的梯度下,更新会更大。有噪声梯度的参数更新较小(因为√v 更大)。
第11部分:模型采样
生成文本时,我们从概率分布中抽样:
工作原理: 想象将所有概率端对端排列在一条线上。随意扔飞镖。无论落在哪个概率区段,都要返回该标记。
示例: 如果概率为 [0.7, 0.2, 0.1]:
- 0 到 0.7: 返回令牌 0
- 0.7 到 0.9: 返回令牌 1
- 0.9 到 1.0: 返回令牌 2
第12部分:训练循环——将一切整合起来
现在我们把所有东西串联成一个训练循环:
为什么要洗牌? 我们希望模型看到文档的顺序是随机的,而不是总是按相同顺序。这防止了对有序的过拟合。
## 训练超参数
这些是小型模型的典型数值。较大的模型通常使用较小的学习率(1e-4 到 1e-5)。
## 训练步骤
代币化: 我们将文本包裹在 BOS 代币中。这为模型提供了清晰的起止边界。
## 前传
交叉熵损失: 对于每个位置,我们计算:
良好的预测(高概率)损失较低。错误的预测会导致高损失。
为什么是 1e-30? 防止未定义的 log(0)。如果概率正好为 0(不应该发生),我们使用一个非常小的值。
## 向后传递与优化
余弦排程: 学习率从 lr,沿余弦曲线下降至末~0。这有助于模型稳定在一个良好的最低限度。
我们会更新每个参数组。看着损失减少!
第13部分:推理——生成文本
训练后,我们从模型中取样:
温度: 控制随机性:
- 温度 = 1.0: 样本准确来自模型分布
- 温度 < 1.0: 更自信(分布更清晰)
- 温度 > 1.0: 更随机(更平坦的分布)
工作原理: 在 softmax 改变分布形状之前,将对数除以温度。
## 清理工作
永远释放你被玛洛卡控制的记忆!
编制与跑步
编译时有优化:
旗帜说明:
- -O3: 最大优化
- -march=native: 使用 CPU 专用指令(AVX 等)
- -ffast-math: 更快的浮点数(交换一定精度)
- -lm: 链接数学库(用于 sqrt、exp 等)
创建一个 包含训练数据的 input.txt(每行一个文档):
跑步:
你应该会看到损失减少,然后才生成样本!

恭喜 你,🎉 你刚刚用纯 C 从零开始构建了 GPT。
我们从随机数发展到可用的语言模型。我们实现了所有组件:注意、反向传播和优化。
下次有人谈论变形金刚或注意力机制时,你不会只是点头附和。你是从零开始用纯 C 语言构建的,没有依赖。
完整代码: https://github.com/vixhal-baraiya/microgpt-c
(如果你觉得有帮助,别忘了给 ⭐ 仓库加上星号!)
继续建造。继续学习。
## 相关链接
- [vixhaℓ](https://x.com/TheVixhal)
- [@TheVixhal](https://x.com/TheVixhal)
- [34K](https://x.com/TheVixhal/status/2022734079167467711/analytics)
- [https://github.com/vixhal-baraiya/microgpt-c](https://github.com/vixhal-baraiya/microgpt-c)
- [升级至高级版](https://x.com/i/premium_sign_up)
- [2:05 AM · Feb 15, 2026](https://x.com/TheVixhal/status/2022734079167467711)
- [34K Views](https://x.com/TheVixhal/status/2022734079167467711/analytics)
- [View quotes](https://x.com/TheVixhal/status/2022734079167467711/quotes)
---
*导出时间: 2026/2/15 09:27:03*