← 返回学习笔记

MiniMind — 阶段 2.2:RMSNorm

AILLMMiniMind

2026-03-21 · 全文

Weight Tying 到底怎么共享的?


第一步:理解两个层各自长什么样

两个层的本质都是一个矩阵(一张表格):

Embedding 矩阵:形状 [6400, 512]
  → 6400 个字,每个字对应一个 512 维的向量

Output 矩阵:  形状 [6400, 512]
  → 把 512 维的向量,映射回 6400 个字的概率

注意:两个矩阵的形状完全一样!


第二步:看看它们各自怎么学习

Embedding(输入时):根据字的编号,去矩阵里查一行

输入:"天" → 编号 38

Embedding 矩阵(6400行 × 512列):
第 0 行: [0.12, -0.34, 0.56, ...]   ← "的"
第 1 行: [0.45,  0.23, 0.11, ...]   ← "是"
  ...
第38行: [0.78, -0.12, 0.93, ...]   ← "天" ✅ 取出这一行!
  ...

输出:[0.78, -0.12, 0.93, ...]  (512维向量)

Output(输出时):用向量和矩阵做乘法,算出每个字的分数

输入:[0.31, 0.67, -0.45, ...]  (512维向量)

用同一个矩阵做乘法:
和第 0 行点乘 → 得分 2.3   ← "的"
和第 1 行点乘 → 得分 1.1   ← "是"
  ...
和第38行点乘 → 得分 5.7   ← "天" ✅ 得分最高!
  ...

输出:预测下一个字是"天"

第三步:所谓”共享”就是——用同一个矩阵!

❌ 不共享:内存中有两个矩阵
┌──────────────────┐    ┌──────────────────┐
│  Embedding 矩阵   │    │  Output 矩阵      │
│  [6400 × 512]    │    │  [6400 × 512]    │
│  占 3.3M 参数     │    │  占 3.3M 参数     │
└──────────────────┘    └──────────────────┘
        总共 6.6M 参数

✅ 共享:内存中只有一个矩阵,两层都指向它
┌──────────────────┐
│  同一个矩阵       │ ← Embedding 用它查行
│  [6400 × 512]    │ ← Output 也用它做乘法
│  占 3.3M 参数     │
└──────────────────┘
        总共 3.3M 参数

第四步:代码里怎么写?就一行!

在 MiniMind 的代码中,大概是这样:

class MiniMindForCausalLM:
    def __init__(self):
        self.model = MiniMindModel(...)
        self.lm_head = nn.Linear(512, 6400)  # Output 层
      
        # ⭐ 关键一行:让 Output 的权重指向 Embedding 的权重
        self.lm_head.weight = self.model.embed_tokens.weight

最后一行就是 weight tying 的全部秘密!

Python 中这样赋值后,lm_head.weightembed_tokens.weight 指向内存中同一块数据。改一个,另一个也跟着变。训练时梯度也会同时更新到这一个矩阵上。


用生活比喻

📖 一本中英词典,你可以:

  • 正着查:知道中文 → 找英文(Embedding:字 → 向量)
  • 反着查:知道英文 → 找中文(Output:向量 → 字)

共享权重 = 只买一本词典,正反两用 不共享 = 买两本词典,一本中→英,一本英→中


大模型不用 Weight Tying 的原因

原因一:省的参数可以忽略不计(可以看昨天的笔记)

小模型 MiniMind:省下 3.3M / 26M  = 12.7%  → 很值!
大模型 LLaMA-65B:省下 262M / 65000M = 0.4% → 无所谓

这个好理解,不再重复。


原因二(核心):两层的”目标”不同,强行共享会互相拖后腿

这是最重要的原因。

这里用一个具体例子来说明

场景设定

假设我们的词表只有 4 个字,向量只有 2 维(方便画图理解):

词表:["猫", "狗", "石", "吃"]

矩阵就是 4 行 × 2 列:

      第1维  第2维
猫:  [ ?,    ? ]
狗:  [ ?,    ? ]
石:  [ ?,    ? ]
吃:  [ ?,    ? ]

Embedding 层想要什么?

Embedding 的任务是理解语义。它希望:

"猫"和"狗"都是动物 → 向量要接近
"猫"和"石"毫无关系 → 向量要远离

所以 Embedding 的理想矩阵

      第1维  第2维
猫:  [ 0.9,  0.8 ]  ← 猫和狗很接近 ✅
狗:  [ 0.8,  0.7 ]  ← 
石:  [-0.5, -0.6 ]  ← 石头离动物很远 ✅
吃:  [ 0.3, -0.2 ]

画成图:

第2维 ↑
 1.0 |      🐱猫(0.9,0.8)
 0.8 |     🐶狗(0.8,0.7)
 0.6 |
 0.4 |
 0.2 |
 0.0 |----------→ 第1维
-0.2 |   🍴吃
-0.4 |
-0.6 | 🪨石

猫和狗紧紧靠在一起,这对 Embedding 来说是好事——语义相近的词应该在一起。


Output 层想要什么?

现在看 Output 怎么学习。假设模型要预测下一个字:

"小明家养了一只___"  → 答案应该是"猫"

Transformer 输出了一个向量 v = [0.85, 0.75]

Output 把 v 和矩阵每一行做点乘,算分数:

和"猫"点乘:0.85×0.9 + 0.75×0.8 = 0.765 + 0.60 = 1.365
和"狗"点乘:0.85×0.8 + 0.75×0.7 = 0.680 + 0.525 = 1.205
和"石"点乘:0.85×(-0.5) + 0.75×(-0.6) = -0.425 + (-0.45) = -0.875
和"吃"点乘:0.85×0.3 + 0.75×(-0.2) = 0.255 + (-0.15) = 0.105

结果:

猫: 1.365  ← 最高 ✅
狗: 1.205  ← 非常接近!⚠️ 危险!
石: -0.875
吃: 0.105

⚠️ 问题出现了!

猫的得分:1.365
狗的得分:1.205
差距只有:0.16  ← 太小了!

因为 Embedding 把猫和狗的向量放得很近,导致 Output 层几乎分不清猫和狗

再看一个更极端的场景:

"这块___头很硬"  → 答案是"石"

Transformer 输出 v = [-0.45, -0.55]

和"猫"点乘:-0.45×0.9 + (-0.55)×0.8 = -0.405 + (-0.44) = -0.845
和"狗"点乘:-0.45×0.8 + (-0.55)×0.7 = -0.36 + (-0.385) = -0.745
和"石"点乘:-0.45×(-0.5) + (-0.55)×(-0.6) = 0.225 + 0.33 = 0.555 ✅
和"吃"点乘:-0.45×0.3 + (-0.55)×(-0.2) = -0.135 + 0.11 = -0.025

这里”石”确实最高,但注意——这是因为”石”和其他词本来就离得远。问题只出在语义相近的词之间。


Output 层理想中的矩阵长什么样?

Output 希望每个字都离得远远的,这样才能精确区分:

      第1维  第2维
猫:  [ 1.0,  0.0 ]  ← 猫指向正东
狗:  [ 0.0,  1.0 ]  ← 狗指向正北(和猫垂直!)
石:  [-1.0,  0.0 ]  ← 石指向正西
吃:  [ 0.0, -1.0 ]  ← 吃指向正南

画成图:

第2维 ↑
 1.0 |    🐶狗
     |
 0.0 🪨石──────🐱猫 → 第1维
     |
-1.0 |    🍴吃

四个字均匀散开,互相离得最远,这样 Output 层就能精确区分每个字。


直观对比:两层的理想矩阵完全不同!

Embedding 的理想:             Output 的理想:
                       
  🐱🐶 靠在一起                🐶
  (语义相似要近)          🪨    🐱
                              🍴
  🪨 远离                  (所有字都要远离)
Embedding 说:"猫和狗应该靠近!它们都是动物!"
Output 说:  "猫和狗必须分开!不然我预测会出错!"

共享一个矩阵 → 它们只能妥协 → 两边都做不到最好

为什么小模型能忍,大模型忍不了?

小模型(词表 6400,512维):
  512 维空间很"挤",能表达的信息有限
  Embedding 和 Output 本来就做不到各自的理想状态
  共享反而像一种"约束",防止小模型记住太多噪音(正则化效果)
  → 共享:两边都凑合,但整体更好 ✅

大模型(词表 32000,8192维):
  8192 维空间很"宽敞",有足够的自由度
  Embedding 可以完美实现"猫狗靠近"
  Output 也可以完美实现"猫狗分开"
  强行共享 = 强行让它们妥协 = 浪费了宽敞的空间
  → 不共享:两边各自发挥,效果更好 ✅

一句话总结

Embedding 想让相似的词靠近,Output 想让所有词远离。 小模型空间小,妥协无所谓;大模型空间大,各自分开效果更好。

原因三:模型越深,两层的”世界观”差距越大

小模型(8层):
  输入 → [8层处理] → 输出
  Embedding 和 Output 离得近,看到的东西差不多
  共享一个矩阵,勉强能兼顾 ✅

大模型(80层):
  输入 → [80层处理] → 输出
  经过 80 层变换后,数据的"形态"已经完全不同了
  Embedding 和 Output 各自需要的矩阵差距巨大
  强行共享 = 两边都做不好 ❌

打个比方:

🏫 小学(8层小模型):语文老师兼数学老师,课程简单,一个人能搞定

🎓 大学(80层大模型):量子物理和古典文学,必须找两个专家,一个人根本兼不了


原因四:实验验证

研究者做过实验对比:

模型规模共享权重不共享结论
小模型效果好 ✅容易过拟合共享更优
大模型效果变差 ❌效果更好 ✅不共享更优

所以 GPT-2(1.5B,较小)用了共享,而 GPT-3(175B)、LLaMA 系列都不再共享


知识点 Transformer Decoder-Only:全局鸟瞰

一段文字进入 MiniMind,经过了哪几步,变成了下一个字?

"今天天气" → [ ? ] → "很"

整个过程就 5 步:

文字 "今天天气"
  ↓ ① Tokenizer:文字变编号
[102, 537, 537, 1893]
  ↓ ② Embedding:编号变向量
[[0.12, -0.34, ...], [0.45, 0.23, ...], ...]
  ↓ ③ Transformer Block × 8层:理解和加工
[[0.56, 0.91, ...], [0.33, -0.78, ...], ...]
  ↓ ④ RMSNorm:稳定数值
[[0.55, 0.90, ...], [0.32, -0.77, ...], ...]
  ↓ ⑤ lm_head:向量变成6400个字的分数,选最高分
"很"

就这么多。每一步做什么,我们后面逐个拆解。

现在你只需要记住这个流水线顺序:

Tokenizer → Embedding → 8层Block → RMSNorm → lm_head


🤔 思考题

① 和 ② 我们已经学过了(Tokenizer 和 Embedding)。剩下的 ③④⑤ 中,你觉得哪一步是最核心、最复杂的?为什么?

回答:我认为是 Transformer Block,因为它要进行多层次的处理和传递

完全正确!✅

③ Transformer Block 就是整个模型的大脑,其他步骤都是”搬运工”。

那我们现在打开这个大脑,看看一个 Block 里面有什么。


一个 Transformer Block 内部长什么样?

MiniMind 有 8 个 Block,每个 Block 内部结构完全相同

输入向量

RMSNorm(稳定数值)

Self-Attention(字和字之间互相看)

残差连接(加回原始输入)

RMSNorm(再次稳定数值)

FFN / SwiGLU(独立深度加工)

残差连接(加回上一步输入)

输出向量

总共就 6 个零件,两两一组,分成 3 对:

零件一句话说明
第1对RMSNorm + Attention先稳定,再让字与字交流
第2对残差连接把原始信息加回来,防止丢失
第3对RMSNorm + FFN先稳定,再让每个字独立思考

🤔 思考题

这 6 个零件里有两个核心计算:AttentionFFN

用一个比喻来理解:

  • Attention = 开会讨论(大家互相交流信息)
  • FFN = 回到工位独立思考(自己消化刚才讨论的内容)

问题:为什么需要先讨论再独立思考,而不是只做其中一个?

回答:因为需要先让字之间相互认识。这里的字是不是token?

严格来说:

  • Token 是 Tokenizer 切出来的最小单位
  • 是我为了方便理解用的通俗说法

在 MiniMind 中,因为词表只有 6400,很多时候一个中文字 = 一个 Token,但不总是。比如:

"今天" → 可能是 1个Token(整词),也可能是 2个Token(逐字)
"hello" → 可能是 1个Token,也可能被切成 "hel" + "lo" 两个Token

所以从现在开始,会统一用 Token 这个准确说法。


回到回答:

因为需要先让 Token 之间相互认识

✅ 对的!Attention 让 Token 之间交换信息,FFN 再对每个 Token 独立加工。缺一不可


接下来我们进入第一个零件:RMSNorm

开始前先问你一个问题:

🤔 思考题

假设 Embedding 输出的向量值是这样的:

Token 1: [0.01, 0.02, 0.01]    ← 数值很小
Token 2: [150.0, 200.0, 180.0]  ← 数值很大

这两个 Token 接下来要一起进入 Attention 做计算。你觉得这会有什么问题?

回答:数值差距很大,所以计算很麻烦?

✅ 回答正确!更准确地说,专业术语叫做数值不稳定(numerical instability)

具体来说有两个问题:

  • 梯度爆炸(gradient explosion):数值太大,训练时参数更新量失控
  • 梯度消失(gradient vanishing):数值太小,参数几乎不更新

所以我们需要在计算前把数值归一化(Normalization),拉到一个稳定的范围。


零件 ①:RMSNorm(Root Mean Square Normalization)

它做了什么?

一句话:把每个 Token 的向量缩放到统一的数值范围

公式:

RMSNorm(x) = x / RMS(x) * γ

其中 RMS(x) = sqrt( mean( x² ) + ε )

用刚才的例子:

Token 2: [150.0, 200.0, 180.0]

① 算平方的均值(mean of squares):
   (150² + 200² + 180²) / 3 = (22500 + 40000 + 32400) / 3 = 31633

② 开根号(root):
   sqrt(31633) ≈ 177.9

③ 每个值除以它:
   [150/177.9, 200/177.9, 180/177.9] ≈ [0.84, 1.12, 1.01]
 
④ 乘以可学习参数 γ(learnable scale parameter):
   γ 初始为 [1, 1, 1],训练中模型自己调整

数值从 [150, 200, 180][0.84, 1.12, 1.01]稳定了!


🤔 思考题

你可能听说过另一种归一化方法叫 LayerNorm。它的公式是:

LayerNorm(x) = (x - mean(x)) / std(x) * γ + β

对比 RMSNorm:

RMSNorm(x) = x / RMS(x) * γ

你能看出 RMSNorm 比 LayerNorm 少了什么操作吗?(提示:看公式的差异)

回答:看起来是少了 ,这里 mean(x)std(x) 是做什么,麻烦解释下

评价:对的,但还少了 均减值的部分(-mean(x)