理解大模型(业余视角)
最近在从事MTK平台的嵌入式大模型部署,在这个部署过程中我发现很多量化的打印信息,下载界面的lora工具之类的东西我都看不明白,于是想方设法找到了一个github仓库:rasbt/LLMs-from-scratch: Implement a ChatGPT-like LLM in PyTorch from scratch, step by step 看完之后我大为震撼,想写下这篇文章记录一下。
1.大模型的结构简易版
首先我们先来看看一个大模型的宏观结构

从图1-1我们可以看到一个简易的大模型首先会一个分词器,切分成一个个token之后进入嵌入层,接着进入transformer block模块,然后进入输出层处理变成一个个输出的token。
2.transformer 模块
一、 宏观架构:三段式处理流水线

GPT 模型在宏观层面上可以划分为三个清晰的数据处理阶段:
- 输入准备阶段:将自然语言文本转换为计算机可理解的连续向量。
- 核心计算阶段:通过堆叠 N 个 Transformer Block,反复进行深度上下文理解与特征提取。
- 输出预测阶段:将经过多层计算的高维特征映射回词表,完成下一个词的预测。
二、 微观解剖:Transformer Block 的核心演化
面对包含大量细节的 19 步拆解图,理解其背后运作的关键在于把握三个核心动作:上下文聚合、独立特征提取、以及信息保真度保障。这三个动作在每一个 Transformer Block 中重复执行两次。
动作一:上下文聚合(因果多头注意力机制)
这是 Transformer 中计算最密集的部分,作用是让当前词汇获取全局上下文信息。整个流程包含:
- 向量映射:将经过归一化处理的输入向量,分别通过三个独立的线性层,映射为查询向量(Q)、键向量(K)和值向量(V)。
- 因果掩码(Causal Mask):因为 GPT 是自回归模型(只能根据前文预测下文),在计算注意力分数时,必须施加“因果掩码”。这保证了计算当前词时,只能关注到自身及之前的词,无法“偷窥”未来的词。
- 缩放点积与 Softmax:计算 Q 和 K 的点积,并经过缩放和 Softmax 函数。注意,Softmax 在这里的作用是将原始分数转化为加权的概率分布,但这个概率矩阵仅仅内部使用。模型拿着这个概率分布去加权求和 V,最终生成的是一个融合了上下文信息的连续特征向量。这个特征向量,而不是概率权重,才是传递给下一层的实际数据。
动作二:独立特征提取(前馈神经网络 FFN)
如果说多头注意力是“广交朋友,搜集情报”,那么前馈网络就是“闭门思考,推演逻辑”。在每一个 Transformer Block 中,经过注意力模块处理后的数据,会进入由两个线性层和一个激活函数组成的前馈网络。
- 升维与降维:数据通常会被扩展到 4 倍以上的维度,在更高维的空间中进行非线性特征变换,然后再被压缩回原始维度。这个过程有助于模型捕捉更复杂、更抽象的特征模式。
- 非线性激活:GPT 系列常使用 GELU 激活函数,与传统的 ReLU 相比,它在保留负值小部分信息上表现更好,使模型具有更强的表达力。
动作三:信息保真度保障(残差连接与层归一化)
在整个计算流中,存在大量残差连接(Shortcut/Skip Connection)。这是 Transformer 架构中最伟大的设计之一。
- 残差连接的操作非常简单:将当前模块的输入原封不动地加到模块的输出上。
- 这一设计有效解决了深层网络中的“梯度消失”问题。即使注意力和前馈网络在处理过程中丢失了原始特征,残差连接也能把底层的原始信息“备份”一份并叠加到输出中,保证下一层始终能获取到原始输入的信息。
- 同样,每个大模块前都会先进行层归一化(LayerNorm),确保数据分布稳定,支撑了千亿级参数大模型的稳定训练。
三、 最终的输出层:预测下一个词
经过 N 个 Transformer Block 的反复处理,序列中每一个 Token 的向量都包含了极其丰富的语义和上下文关系。
在最后的输出阶段:
- 模型会对最终输出的矩阵再做一次层归一化。
- 经过一个名为“LM Head”的线性投影层,将高维隐藏空间映射回词表大小(例如 50,000 个词)的维度。
- 生成各个词汇的概率分布(Logits),最终通过 Softmax 选取得分最高的一个词,作为模型“生成”的下一个词汇。
3.对transformer 直觉上的提问
3.1“线性”和”非线性”到底是什么意思?
先看线性
一个函数是线性的,意味着:把输入放大两倍,输出也放大两倍。 数学上满足两条:
f(a + b) = f(a) + f(b) // 和的输出 = 输出的和 f(k × a) = k × f(a) // 缩放的输出 = 输出的缩放
矩阵乘法就是最典型的线性操作:
假设 W 是一个权重矩阵 output = W @ input # 矩阵乘法,纯线性
线性性质: W @ (a + b) = W@a + W@b ✅ W @ (3 × a) = 3 × (W@a) ✅
那非线性是什么?
任何不满足上面两条的函数。比如:
ReLU f(x) = max(0, x)
检验:f(2 + (-3)) = f(-1) = 0 # 但 f(2) + f(-3) = 2 + 0 = 2 # 0 ≠ 2 → 不满足,所以是非线性的
GELU(更光滑的版本) f(x) = x × Φ(x) # Φ 是正态分布的累积分布函数 # 也不满足线性条件
为什么非线性这么重要?
想象你要用一个直线函数去拟合一条曲线——你无论怎么组合直线,结果还是直线:
每层都是 x → Wx + b
堆叠三层: 第 1 层:W₁x + b₁ 第 2 层:W₂(W₁x + b₁) + b₂ = W₂W₁x + W₂b₁ + b₂ 第 3 层:W₃(…) + b₃
= W₃W₂W₁x + 某个常数
最终:output = W_total × x + constant
↑ 还是线性!堆 100 层也等价于 1 层!
不加非线性激活函数,一万层网络 = 一层网络。 因为线性变换的组合还是线性变换。你在墙上凿了三个门,但你每次走出门又立刻转弯——回到数学上,三个步骤其实等价于一个步骤。
非线性激活插在中间,把这种”等价性”打破了:
线性 → ReLU/GELU → 线性 → ReLU/GELU → 线性 ↑ ↑ ↑ 每一个连接处都被非线性"掰弯",组合起来能拟合任意复杂函数
3.2 GELU 是什么?
GELU = Gaussian Error Linear Unit。你 notebook 里的代码:
class GELU(nn.Module):
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))
))
直观理解:它是 ReLU 的”光滑版”
ReLU: 负数 → 直接砍成 0
正数 → 原样通过
GELU: 负数 → 不是完全砍成 0,而是给一个很小的负值(概率性的)
正数 → 基本原样通过,但稍微弯曲
把它想成:不是简单地”负数就扔掉”,而是根据正态分布的概率来决定保留多少。 如果 x 大概率在正半轴,就保留接近全量;如果 x 大概率在负半轴,就只留一点点。
对比一下:
x: -3 -2 -1 0 1 2 3 ReLU: 0 0 0 0 1 2 3 ← 负数一刀切 GELU: -0.01 -0.05 -0.16 0 0.84 1.95 2.99 ← 负数有个"小尾巴"
为什么 GELU 比 ReLU 好?
ReLU 在负半轴的梯度是零——神经元一旦走到负数区域,就彻底”死”了,再也收不到梯度更新。GELU 在负半轴保留了一点点非零梯度,让这些神经元还有机会被救回来。GPT-2 和 BERT 用的都是 GELU。
3.3为什么说注意力是”线性”的?
回看 MultiHeadAttention 内部做了什么:
回看 MultiHeadAttention 内部做了什么:
① Q = W_q × x ← 线性(矩阵乘法)
② K = W_k × x ← 线性(矩阵乘法)
③ V = W_v × x ← 线性(矩阵乘法)
④ attention_scores = Q × K^T / sqrt(d_k) ← 线性(矩阵乘法)
⑤ attention_weights = softmax(attention_scores)
↓
这步引入了非线性!
softmax 不是线性操作。那为什么说注意力”整体偏线性”?
因为 softmax 的核心作用是归一化(把分数变成概率分布,和为一),它引入的非线性非常有限。注意力输出的本质操作还是:
output = attention_weights × V
= Σ (每个位置的 V 乘以一个权重)
= V 的加权求和
↑
这本质上就是线性组合!不同的只是"权重怎么算出来的"
它是把一个位置的信息搬到另一个位置——搬运工(注意力权重)可以是非线性的,但搬运的动作本身还是加权求和。
和 FFN 的对比就很清楚了:
┌────────────┬────────────────────────────────| │ │ 注意力 │ FFN │ ├────────────┼────────────────────────────────| │ 核心操作 │ Σ w_i × V_i(加权求和) │ W₂ × GELU(W₁ × x) │ ├────────────┼────────────────────────────────| │ 非线性来源 │ softmax(弱非线性,主要做归一化) │ GELU(强非线性,引入弯曲) │ ├────────────┼────────────────────────────────┤ │ 主要功能 │ 搬信息——聚合其他 token 的内容 │ 加工信息——在每个 token 内部做深度变换 ├────────────┼──────────────────────────────── │ 如果没有它 │ token 之间无法交流 │ 表达能力坍缩为线性模型 │ └────────────┴────────────────────────────────
▎ 注意力负责”从哪里拿信息”,FFN 负责”拿到之后怎么处理”。一个管路由,一个管加工。
3.4多头注意力后面为什么要接一个前馈神经网络(FFN)?
简短回答
注意力负责”搬信息”,FFN 负责”消化信息”——两个模块各司其职,互不冗余。
详细解释
多头注意力本质上是一个动态的信息聚合器:
- Q 去搜索,K 去匹配,V 提取信息
- 输出是对其他 token 信息的加权求和(纯线性组合)
FFN(768 → 3072 → GELU → 768)做三件注意力做不到的事:
| 作用 | 说明 |
|---|---|
| 引入非线性 | 注意力全是线性操作(矩阵乘法 + softmax 加权求和)。没有 FFN 的 GELU 非线性,堆多少层都等价于一层线性变换 |
| 存储事实知识 | 研究表明 Transformer 的大部分事实性知识存储在 FFN 权重中。中间层扩展到 4 倍维度(768→3072)提供了巨大的记忆容量 |
| 逐 token 独立加工 | 注意力在 token 之间”交流”,FFN 对每个 token 独自做深度处理——听取完所有人发言后,自己消化思考 |
类比:注意力 = 会议上听每个人发言;FFN = 自己安静下来消化、形成新见解。
3.5残差连接(shortcut / skip connection)的直觉是什么?
简短回答
不扔掉旧认知,而是在旧认知基础上叠加新领悟。同时给梯度开一条”高速公路”直通最底层。
详细解释
没有残差连接:10 层纯串行后,第 1 层的梯度需要穿过 10 个乘法,指数级衰减 → 梯度消失 → 前几层是”死的”。
残差连接的数学魔法:
x = x + shortcut # shortcut 就是进入子层之前的原始输入
梯度在反向传播时分成两条路:
∂Loss/∂x = ∂Loss/∂output × 1 ← 直通线,不经过子层,不衰减
+ ∂Loss/∂output × ∂(子层)/∂x ← 穿过子层的那条路
即使子层的梯度趋近于零,那条 ×1 的直通线仍然把信号完整地传回最底层。
3.6为什么经过一个 Transformer Block 之后还要再进下一个?
简短回答
一层注意力只能看到”表面关系”,复杂理解需要多次、分层的信息聚合。
详细解释
每层的输入不同 → 关注点自然分化(这是训练中涌现的,不是人为设定的)一下是例子:
| 层 | 多头注意力关注 | FFN 消化产出 |
|---|---|---|
| 浅层(1-3) | 邻近词的语法关系(形容词修饰哪个名词、主语-动词) | 初级 token 表示 |
| 中层(4-7) | 短语/从句级语义关系 | 短语级语义特征 |
| 深层(8-12) | 句子/段落级逻辑(因果、转折、共指消解) | 高层语义表示 |
核心原因:
- 单层表达能力有限:一层注意力只能算一种”成对 token 的加权聚合”。无法同时捕捉语法依赖、共指消解、情感反转、长程因果等多种不同粒度的关系
- 层次化抽象是自然涌现的:第 1 层的输入是原始 embedding,第 6 层的输入是前 5 层 FFN 反复加工过的——原材料不一样,关注的东西自然会分化
- 真实观测验证:可解释性研究已观测到——浅层注意力看邻近 token,深层注意力越来越全局化,部分深层头甚至变成”空头”传递全局语境
关键:12 层的代码结构
完全相同(
[TransformerBlock(cfg) for _ in range(12)]),分工不是人为设定的,是梯度下降在训练中
自己涌现的。
3.7串起来:整个 Transformer Block 的计算流
输入 x(一个 token 的向量表示)
│
├─→ 注意力:去邻居那搬信息,做加权求和
│ └─→ 结果:混入上下文信息的表示
│
├─→ 残差:加上原来的 x(别把老本丢了)
│
├─→ FFN 第一层:768 维膨胀到 3072 维
│ └─→ 在高维空间"展开"信息
│
├─→ GELU:非线性掰弯!(这是整个 block 里最关键的非线性来源)
│ └─→ 打破纯线性链条,赋予表达能力
│
├─→ FFN 第二层:3072 维压缩回 768 维
│ └─→ 把加工完的信息"浓缩"回来
│
├─→ 残差:加上注意力输出(别把上下文信息丢了)
│
└─→ 输出:包含上下文信息 + 深度加工后的 token 表示
4.具体分模块解释
4.1从字符到多头注意
第 1 步:字符 → Token(分词)
输入文本:今天天气真
- 分词器(BPE) 会把中文拆成子词或字节。为了清晰,我们假设它被拆成了 5 个独立的 Token:
[ "今", "天", "天", "气", "真" ]
- 查词表,得到对应的 Token ID(假设):
[ 1024, 2048, 2048, 3072, 4096 ]
第 2 步:Token → 嵌入向量(Embedding)
每个 Token ID 去查找嵌入矩阵(nn.Embedding),变成一个稠密向量(比如 12 维)。
"今"→[0.1, -0.2, 0.3, ...](长度 12)"天"→[0.4, 0.1, -0.5, ...]"天"→ 同上"气"→[0.2, 0.7, 0.1, ...]"真"→[-0.3, 0.5, 0.2, ...]
此时,输入矩阵 X 的形状是 (5, 12)(5 个词,每个 12 维)。
第 3 步:加入位置编码(Positional Encoding)
模型需要知道词的顺序。GPT-2 使用可学习的位置嵌入,为每个位置(1~5)加上一个随机初始化的向量。
- 位置 1 的向量 +
"今"的向量 = 新的x1 - 位置 2 的向量 +
"天"的向量 = 新的x2 - ……
此时,X 变成了包含位置信息的 (5, 12) 矩阵。但注意:这里的“位置 1”只代表它在序列里的绝对序号,模型此时并不知道“天”和“天气”的语义关联,它只看到一堆数字。
第 4 步:QKV 计算(生成查询、键、值)
输入的 X (5, 12) 分别乘以三个权重矩阵 W_Q, W_K, W_V(大小均为 12 x 12),生成 Q、K、V。
- Q(Query):
X @ W_Q→ 形状(5, 12)。每一行代表“这个词想问什么?” - K(Key):
X @ W_K→ 形状(5, 12)。每一行代表“这个词能回答什么?” - V(Value):
X @ W_V→ 形状(5, 12)。每一行代表“这个词的实际内容是什么?”
具体到“真”这个词(第 5 个位置):
- 它的查询向量
q5长度为 12,比如[0.8, 0.1, ...],它想知道“前面哪个词能补充我?” - 它的键向量
k5和值向量v5也同时算出来了。
第 5 步:计算注意力分数(Q 和 K 的点积)
现在,计算所有词对所有词的关注度:Attention_Scores = Q @ K^T。
- 形状:
(5, 12) @ (12, 5) = (5, 5)。 - 这个 5x5 矩阵里,第 i 行第 j 列 的数字,代表 第 i 个词对第 j 个词的关注程度(未归一化)。
比如:
(0, 0):"今"对"今"的关注度。(4, 1):"真"对"天"的关注度。
第 6 步:因果掩码(Causal Masking)—— GPT 的核心!
这是 GPT 能“续写”的关键。为了防止模型看到未来,我们把上三角矩阵(j > i)全部填成 -inf(负无穷)。
| 行\列 | 今(0) | 天(1) | 天(2) | 气(3) | 真(4) |
|---|---|---|---|---|---|
| 今(0) | 2.3 | -inf | -inf | -inf | -inf |
| 天(1) | 0.5 | 3.1 | -inf | -inf | -inf |
| 天(2) | 0.1 | 2.2 | 1.8 | -inf | -inf |
| 气(3) | 0.2 | 1.1 | 2.0 | 2.5 | -inf |
| 真(4) | 0.3 | 0.8 | 0.9 | 1.2 | 1.5 |
注意看最后一行("真"):它可以看到前面的 今、天、天、气 以及自己,但看不到未来(因为没有未来)。
第 7 步:Softmax 归一化(变成概率权重)
对每一行单独应用 Softmax:
- 因为
-inf经过 Softmax 后会变成0,所以未来位置的权重被清零。 - 每一行的数字变成了概率分布,且行内求和为 1。
只看最后一行("真" 的行):
假设 Softmax 后,"真" 对前面 5 个词的注意力权重分别是:
[0.1, 0.2, 0.2, 0.25, 0.25]- 这意味着:当模型处理
"真"时,它最关注的是它自己(0.25)和前一个词"气"(0.25),它们共同决定了"真"要补充什么信息。
第 8 步:生成上下文向量(加权求和 Value)
用第 7 步的权重,去加权第 4 步算出来的 V(Value)矩阵:
context_vec = Attention_Weights @ V- 形状:
(5, 5) @ (5, 12) = (5, 12)。
对于 "真" 这个词(第 5 行),它的新向量是前面 5 个词 Value 向量的加权混合。这意味着,"真" 的最终向量里,包含了 "今天天气" 的语义信息。
第 9 步:输出层(LM Head)预测下一个词
现在,我们只需要最后一个位置的向量(位置 5,对应 "真" 的那一行,长度 12)。
- 这个向量通过一个线性层(
nn.Linear(12, vocab_size)),映射到整个词表(比如 50,257 个词)。 - 得到
logits(未归一化的分数),形状(1, 50257)。 - 应用 Softmax,得到每个词的概率。
假设在词表中:
"好"对应的 ID 概率是 0.85(最高)。"棒"概率是 0.10。"差"概率是 0.02。
第 10 步:采样生成(选择“好”)
模型现在有一个概率分布。通常我们会用 torch.multinomial 进行抽样(或者直接取最高概率 argmax)。
- 由于种子固定(
manual_seed(123)),随机数生成器被锁定。假设这次抽到了概率最高的 0.85。 - 结果:模型选中了 Token ID 对应的
"好"。 - 分词器把
"好"还原成文字,输出给用户。
4.2 前馈网络以及残差连接到输出
第 1 步:进入前馈网络前的独立归一化(Pre-LayerNorm 2)
- 动作与逻辑:数据进入前馈网络前,首先经过独立的层归一化模块。请注意,这个 LayerNorm 的参数与注意力层之前的那个 LayerNorm 是完全独立的,它们各自承担不同的特征稳定任务。
- 数据形态:输入张量 hatthatt 形状为
(1, 5, 12)。经过归一化后,特征数据的均值和方差被调整为标准正态分布,但张量形状不变,输出仍为(1, 5, 12)。
第 2 步:特征升维扩展(FFN Linear 1)
- 动作与逻辑:归一化后的张量进入第一个线性层。为了在高维空间揭示更多潜在语义特征,模型会将特征维度扩展。
- 数据形态:例如执行
nn.Linear(12, 48)的矩阵乘法,将隐藏特征从 12 扩展到 4 倍,变为 48。输出的张量形状增长为(1, 5, 48)。
第 3 步:引入 GELU 非线性激活(激活层)
- 动作与逻辑:高维特征进入激活函数层。现代 GPT 模型普遍采用 GELU(高斯误差线性单元)。不同于 ReLU 直接硬截断负数,GELU 允许极少部分的负值以极其微弱的比例平滑保留。
- 数据形态:形状保持
(1, 5, 48)不变,但张量内部的具体数值经过平滑的非线性映射,赋予了模型拟合复杂连续语义的能力。
第 4 步:特征降维浓缩(FFN Linear 2)
- 动作与逻辑:经过高度非线性变换的向量进入第二个线性层。这一步负责“提炼收敛”,将刚才在高维空间探索到的复杂组合信息,压缩回原始的隐藏层维度。
- 数据形态:执行
nn.Linear(48, 12)的矩阵乘法,张量形状恢复为(1, 5, 12)。 - 意义:至此,前馈神经网络内部的“闭门思考”流程彻底完结,输出结果记为 hffnhffn。
第 5 步:防过拟合(Dropout)与信息保底(残差相加 2)
- 动作与逻辑:在把前馈网络结果传出前,系统需要做最后两道安全防护。
- 处理一(Dropout):对 hffnhffn 施加 Dropout 正则化,随机丢弃部分神经元连接,防止模型在训练阶段对特定特征产生过拟合。
- 处理二(残差相加):执行图中标注的
+ Shortcut操作。将之前第 1 步进入时的原始输入 hatthatt,直接与经过 Dropout 后的 hffnhffn 输出进行逐元素相加。公式为:hfinal=hatt+Dropout(hffn)hfinal=hatt+Dropout(hffn)。 - 结果:输出最终张量 hfinalhfinal,形状恢复为
(1, 5, 12)。
【延伸衔接】
经过这 5 步操作后产出的 hfinalhfinal,标志着这一个 Transformer Block 的全部功能执行完毕。在实际大规模神经网络的架构中:
- 如果还有
N层 Transformer Block,这个 hfinalhfinal 会直接作为下一个 Transformer Block 的输入,重新进入其注意力机制层,重复上述完整流程。 - 如果已经是第
N层(最后一个 Block),那么这个 hfinalhfinal 将被输送给最终的输出层(LM Head),随后便会经历您示例中提到的“第 9 步:输出层预测下一个词”以及“第 10 步:采样生成”流程,最终完成下一个文字的预测。
更多推荐
所有评论(0)