最近在从事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 模型在宏观层面上可以划分为三个清晰的数据处理阶段:

  1. 输入准备阶段:将自然语言文本转换为计算机可理解的连续向量。
  2. 核心计算阶段:通过堆叠 N 个 Transformer Block,反复进行深度上下文理解与特征提取。
  3. 输出预测阶段:将经过多层计算的高维特征映射回词表,完成下一个词的预测。

二、 微观解剖: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 的向量都包含了极其丰富的语义和上下文关系。
在最后的输出阶段:

  1. 模型会对最终输出的矩阵再做一次层归一化。
  2. 经过一个名为“LM Head”的线性投影层,将高维隐藏空间映射回词表大小(例如 50,000 个词)的维度。
  3. 生成各个词汇的概率分布(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 信息的加权求和(纯线性组合)

FFN768 → 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)句子/段落级逻辑(因果、转折、共指消解)高层语义表示

核心原因:

  1. 单层表达能力有限:一层注意力只能算一种”成对 token 的加权聚合”。无法同时捕捉语法依赖、共指消解、情感反转、长程因果等多种不同粒度的关系
  2. 层次化抽象是自然涌现的:第 1 层的输入是原始 embedding,第 6 层的输入是前 5 层 FFN 反复加工过的——原材料不一样,关注的东西自然会分化
  3. 真实观测验证:可解释性研究已观测到——浅层注意力看邻近 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.53.1-inf-inf-inf
天(2)0.12.21.8-inf-inf
气(3)0.21.12.02.5-inf
真(4)0.30.80.91.21.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)。

  1. 这个向量通过一个线性层(nn.Linear(12, vocab_size)),映射到整个词表(比如 50,257 个词)。
  2. 得到 logits(未归一化的分数),形状 (1, 50257)
  3. 应用 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 步:采样生成”流程,最终完成下一个文字的预测。

更多推荐