【4】为什么模型总喜欢先把维度拉高,再压回来?从一个小例子走到 Transformer、LLaMA 与 LoRA

AI7小时前更新 beixibaobao
2 0 0

写在前面

很多人可能都会好奇:为什么模型里的维度总是被先拉高,再降回来?

我们从一个例子来切入

假设你手里有一批高考生,你要帮他们做志愿填报。

  • 可你一开始只知道一个信息:他们大多都是 171717191919 岁。
  • 这个显然是真的,但区分度几乎为零!

只看年龄,你根本没法判断谁更适合冲理工,谁更适合读医学,谁应该优先考虑城市,谁其实更在意专业方向。

于是你开始不断补充特征:数学成绩、语文成绩、选科组合、是否愿意离家、对城市层级的偏好、家庭预算、职业兴趣。

再往前走一步,你会发现真正有用的甚至不只是这些单独特征,而是它们的组合关系。比如“数学强 + 物理强 + 愿意去外地”和“语文强 + 表达好 + 更看重城市资源”,即使这两个学生高考总分、年龄一模一样,也会得到完全不同的志愿建议。

这个瞬间,其实已经把本文最重要的问题说出来了:

为什么有时候非得先把维度升上去?
因为这样,会让很多原本分不开、讲不清、组合不出来的结构,瞬间变得容易处理;
那为何做完这步,又必须再把维度降回来

本文将会沿着这条主线展开:模型为什么要不断地重新组织表示空间?

  • 先用直觉和小例子把问题讲清楚
  • 再补线性代数工具
  • 最后自然过渡到 Transformer 的注意力与 FFN
  • 并延伸到 LoRALLaMA 的相关结构

阅读路线

  • 第 1 章先用例子把问题立住:为什么有时必须先升维,才能把东西分开。
  • 第 2 章补最少够用的线性代数:空间、秩、投影、SVD。
  • 第 3-5 章再进入神经网络主线:表示、注意力、FFN 到底在怎样重排空间。
  • 第 6-7 章作为延伸:为什么 LoRA 是低秩这条线的自然结果,为什么 LLaMA 会把升降维结构写得更显眼。
  • 第 8 章最后回到工程配置:以后看到 q_projup_projlora_rank 时,应该脑子里先冒出什么图景。

第 1 章 先从一个问题开始:为什么有时非得先升维,再降维?

1.1 先别急着看模型,先看一个生活场景

假设你在做高考志愿填报,手里有同一批学生。你想给他们做更合理的志愿建议:谁更适合偏工科路线,谁更适合偏医学路线,谁应该优先考虑城市平台,谁应该优先考虑专业确定性。

一开始你可能只看两个特征:

  • x1x_1x1:数学/理科能力强不强
  • x2x_2x2:语言表达/文科能力强不强

你很快发现一个尴尬问题:只在这两个维度里画一条线,怎么都切不漂亮。 有的学生是“理科强但表达弱”,有的是“表达强但理科弱”,他们未必一个就一定适合文科、另一个就一定适合理工。真正影响建议结果的,往往不是单个特征本身,而是特征之间的组合关系

这时一个非常自然的想法就出现了:既然二维里不好分,那我能不能先把表示换一下?比如除了看 x1x_1x1x2x_2x2,再额外看一个组合特征 x1x2x_1x_2x1x2,或者更一般地,看一组新的组合坐标。

一旦你这么想,其实就已经站在神经网络设计的门口了。很多模型层本质上都在重复三步:

  1. 先把原表示送到一个更容易展开组合关系的空间里
  2. 在那个空间里做匹配、筛选、非线性组合
  3. 再把结果压回主干空间,继续往下传

从“同一批学生如何被分开”的角度看,这些层都在回答同一个问题:

当原来的表示不够好用时,我们该怎样换一个空间,让结构更容易被分开、被组合、被更新?

你甚至可以先把这件事概括成一行:

训练里的很多“投影”≈换一个更合适的表示空间
text{训练里的很多“投影”} approx text{换一个更合适的表示空间}
训练里的很多投影换一个更合适的表示空间

差别只在于,这个“更合适”到底是:

  • 让表示更好分开
  • 让计算更便宜
  • 让更新更克制
  • 还是让结构更容易模块化复用

1.2 一个直觉版本的统一理解

可以先把“升维”和“降维”想象成在做两种不同的资源分配。

  • 升维:给表示更多活动空间,让模型有能力表达更细的结构差异。
  • 降维:把表示或更新压回一个更小的子空间,让模型只保留最关键的方向。

这就像你在整理资料。

  • 当你只是把一堆便签随手堆在一起时,信息很挤,也容易混。
  • 当你给它们分门别类、开更多抽屉时,本质上是在“升维”,让不同因素能被分开表达。
  • 但抽屉太多也会混乱、占空间,所以你又会把重复内容归档、压缩成几条主线,这又是在“降维”。

模型也是这样:

  • 如果表示一直挤在一个狭窄空间里,很多因素会纠缠在一起,难以分开处理。
  • 如果表示无限膨胀而不回收,参数量、显存和计算都会迅速失控。

所以很多经典结构都长成了一个重复模式:

升维  →  非线性或结构化处理  →  降维
text{升维} ;rightarrow; text{非线性或结构化处理} ;rightarrow; text{降维}
升维非线性或结构化处理降维

1.2.1 一个具体数值小例子:二维 XOR 不可分,但升到三维就可分

为了把“升维到底在帮什么忙”讲得更落地,我们用一个最经典、最干净的小数据集:XOR。

我们有四个二维点(可以想象成“同一批人”的两个二值特征):

(0,0),(0,1),(1,0),(1,1)
(0,0),(0,1),(1,0),(1,1)
(0,0),(0,1),(1,0),(1,1)

定义 XOR 标签(只要两个特征恰好有一个为 1,则为正类):

y=x1⊕x2
y = x_1 oplus x_2
y=x1x2

也就是:

  • 正类(y=1y=1y=1):(0,1)(0,1)(0,1)(1,0)(1,0)(1,0)
  • 负类(y=0y=0y=0):(0,0)(0,0)(0,0)(1,1)(1,1)(1,1)

第一步:说明二维里用一条直线分不开(线性不可分)。

假设存在二维线性分类器:

f(x)=sign(ax1+bx2+c)
f(x)=mathrm{sign}(a x_1 + b x_2 + c)
f(x)=sign(ax1+bx2+c)

要把 XOR 分开,就必须同时满足下面四个不等式(把正类判为正、负类判为负):

f(0,1)>0⇒b+c>0f(1,0)>0⇒a+c>0f(0,0)<0⇒c<0f(1,1)<0⇒a+b+c<0
begin{aligned}
f(0,1)>0 &Rightarrow b + c > 0 \
f(1,0)>0 &Rightarrow a + c > 0 \
f(0,0)<0 &Rightarrow c < 0 \
f(1,1)<0 &Rightarrow a + b + c < 0
end{aligned}
f(0,1)>0f(1,0)>0f(0,0)<0f(1,1)<0b+c>0a+c>0c<0a+b+c<0

把前两条加起来得到:

a+b+2c>0
a + b + 2c > 0
a+b+2c>0

再结合 a+b+c<0a+b+c<0a+b+c<0,两式相减得到 c>0c>0c>0,这与 c<0c<0c<0 矛盾。

所以 XOR 在二维里没有任何一条直线能把正负类分开。

下面这张图把 XOR 的四个点画在二维平面里(红色是正类,蓝色是负类)。你会直观看到:它们呈现“对角交叉”的结构,因此想用一条直线干净切开几乎是不可能的。

xor_2d_points

如果你还想更具体一点,这里给一个“随便选一条直线”的失败示意:我们画出一个线性分类器的分割线与它的决策区域,你会看到总会有点被分错(图中用黄色圈出了误分点)。这不是这条线选得不好,而是 XOR 在二维里本来就线性不可分。

xor_2d_linear_separator_fail

第二步:升到三维,用一个简单的特征变换就能线性可分。

我们做一个非常简单的升维映射:加一个“交叉特征”:

ϕ(x)=[x1x2x1x2]∈R3
phi(x)=
begin{bmatrix}
x_1\
x_2\
x_1x_2
end{bmatrix}
inmathbb{R}^3
ϕ(x)=x1x2x1x2R3

把四个点映射到三维后分别是:

ϕ(0,0)=(0,0,0)ϕ(0,1)=(0,1,0)ϕ(1,0)=(1,0,0)ϕ(1,1)=(1,1,1)
begin{aligned}
phi(0,0)&=(0,0,0) \
phi(0,1)&=(0,1,0) \
phi(1,0)&=(1,0,0) \
phi(1,1)&=(1,1,1)
end{aligned}
ϕ(0,0)ϕ(0,1)ϕ(1,0)ϕ(1,1)=(0,0,0)=(0,1,0)=(1,0,0)=(1,1,1)

现在我们给出一个具体的三维超平面(线性分类器):

g(x)=w⊤ϕ(x)+b,w=[11−2],b=−12
g(x)=w^top phi(x) + b,quad
w=
begin{bmatrix}
1\
1\
-2
end{bmatrix},
quad b=-frac{1}{2}
g(x)=wϕ(x)+b,w=112,b=21

逐点代入计算:

g(0,0)=(1,1,−2)⋅(0,0,0)−12=−12<0g(0,1)=(1,1,−2)⋅(0,1,0)−12=1−12=12>0g(1,0)=(1,1,−2)⋅(1,0,0)−12=1−12=12>0g(1,1)=(1,1,−2)⋅(1,1,1)−12=(1+1−2)−12=−12<0
begin{aligned}
g(0,0) &= (1,1,-2)cdot(0,0,0) – tfrac{1}{2} = -tfrac{1}{2} < 0 \
g(0,1) &= (1,1,-2)cdot(0,1,0) – tfrac{1}{2} = 1 – tfrac{1}{2} = tfrac{1}{2} > 0 \
g(1,0) &= (1,1,-2)cdot(1,0,0) – tfrac{1}{2} = 1 – tfrac{1}{2} = tfrac{1}{2} > 0 \
g(1,1) &= (1,1,-2)cdot(1,1,1) – tfrac{1}{2} = (1+1-2) – tfrac{1}{2} = -tfrac{1}{2} < 0
end{aligned}
g(0,0)g(0,1)g(1,0)g(1,1)=(1,1,2)(0,0,0)21=21<0=(1,1,2)(0,1,0)21=121=21>0=(1,1,2)(1,0,0)21=121=21>0=(1,1,2)(1,1,1)21=(1+12)21=21<0

你会发现:在三维空间里,我们用一个超平面就把 XOR 的正负类分开了。

这张图展示了升维后的 3D 空间(ϕ(x)=(x1,x2,x1x2)phi(x)=(x_1,x_2,x_1x_2)ϕ(x)=(x1,x2,x1x2))里,超平面 g(x)=w⊤ϕ(x)+b=0g(x)=w^topphi(x)+b=0g(x)=wϕ(x)+b=0(半透明灰色)是如何把四个点分开的。

xor_3d_plane_separator

这就是“升维”的一个最本质价值:原空间里需要弯弯绕绕的决策边界,可能在高维里变成一刀切的线性边界。后面你看到 FFN 的升维、门控非线性、再降维,本质上就是在自动学习大量类似 x1x2x_1x_2x1x2 这样的交叉特征,只不过规模大得多、而且是端到端学出来的。

1.2.2 贯穿例子:一个学生的志愿问题,如何“检索”出更合适的建议

上面的 XOR 是一个“极简二维玩具”。现在我们用一个更贴近现实、也更适合贯穿全文的例子:对某一个具体学生,给出更合理的志愿建议

这一章里你只需要抓住一个直觉:你在做志愿咨询时,往往不是“凭空算出来”的,而是会在脑子里(或案例库里)快速检索:

  • 和他成绩结构相近的人当年怎么选
  • 这条路线常见的坑是什么
  • 这个偏好组合通常会把人引向哪几类学校/专业

这件事非常像注意力:给定一个“当前学生的志愿问题”,去匹配一堆“候选信息片段”,再把最相关的内容加权汇总。

我们把符号一次性定下来(后面第 3-6 章会反复复用同一套符号):

  • 当前学生:sss,其向量表示为 x∈Rdmodelx in mathbb{R}^{d_{model}}xRdmodel
  • 当前问题(例如“理科强、想留省内、预算一般,志愿怎么填”):q∈Rdmodelq in mathbb{R}^{d_{model}}qRdmodel
  • 一个案例库/知识库,包含 NNN 条候选条目 {ci}i=1N{c_i}_{i=1}^{N}{ci}i=1N
    • 每条条目可以是“历史学生画像 + 结果”、也可以是“专家规则片段”、也可以是“学校/专业信息块”
    • 每条条目的向量表示为 zi∈Rdmodelz_i in mathbb{R}^{d_{model}}ziRdmodel

然后我们用 Q/K/V 的方式把“当前问题”变成查询,把“候选条目”变成可匹配的键和值:

q′=qWQ,ki=ziWK,vi=ziWV
q' = qW_Q,qquad k_i = z_i W_K,qquad v_i = z_i W_V
q=qWQ,ki=ziWK,vi=ziWV

注意力权重与汇总就是:

αi=softmax(q′ki⊤dk),out=∑i=1Nαivi
alpha_i = mathrm{softmax}left(frac{q' k_i^top}{sqrt{d_k}}right),qquad
mathrm{out} = sum_{i=1}^{N} alpha_i v_i
αi=softmax(dkqki),out=i=1Nαivi

这段看起来像公式,但直觉上非常像“资深老师的思考过程”:

  • qqq 是你当前正在回答的志愿问题
  • kik_iki 是案例库里每条信息的“索引卡片”(方便匹配)
  • viv_ivi 是这条信息真正能贡献的“内容”(能用于生成建议)
  • αialpha_iαi 是“这条信息在当前问题下有多相关”

这个例子之所以适合贯穿全文,是因为它能把后面几章自然串成一条线:

  1. 第 3 章 Embeddingxxxqqqziz_izi 从哪来?离散特征、文本描述、偏好约束如何变成可训练向量
  2. 第 4 章 Q/K/V:为什么要把同一个语义对象拆成 q′/ki/viq'/k_i/v_iq/ki/vi 三种角色(用不同投影承担不同职责)
  3. 第 5 章 FFN:为什么要把表示先升维到 dffd_{ff}dff 再降回 dmodeld_{model}dmodel?可以理解成“自动造交叉特征”,让匹配更容易发生
  4. 第 6 章 LoRA:当策略口径变了(比如更看重就业、城市或专业稳定性)怎么办?不重建整个空间,只用低秩更新 ΔWDelta WΔW 快速挪动关键方向

为了先看一下这个贯穿例子的整体效果,这里给一个自上而下的示意图(重点是“当前问题检索案例库”,而不是“学生之间互相加权”):

当前问题检索案例库的示意图

神经网络里的很多设计,本质上就在做这种事情。它不是真的执着于维度数字本身,而是在调节:

  • 表征容量
  • 参数预算
  • 计算成本
  • 优化稳定性
  • 泛化与遗忘之间的平衡

如果只用一句话概括,可以是:

升维往往不是为了“信息更多”,而是为了“把可分的因素分开”;降维往往不是为了“信息更少”,而是为了“把自由度收拢到最值钱的方向上”。

这句话听起来抽象,但对应到 Transformer 的几个经典部件会非常具体:

  1. Embedding 的升维:把离散 token 变成稠密向量,允许“相似词”在空间里靠近
  2. Q/K/V 的投影:把同一个隐藏状态拆成不同用途的表示(检索键、检索请求、内容载体)
  3. FFN 的升维再降维:给非线性组合留空间,然后把结果压回主干维度
  4. LoRA 的低秩:不是让激活先降维再升维,而是让“可训练更新的自由度”先被压到一个秩为 rrr 的瓶颈里

为了帮助你后面快速对号入座,这里给一个“训练里常见 projection”的总览图(自上而下布局,后面章节会逐个展开):

训练中常见投影机制的总览图

字体的环境依赖缘故,图中汉字为『门』控

1.3 这篇文章里“projection”有三层含义(以及一个命名陷阱)

后面我们会频繁用到 projection,但必须先把语义拆开,不然后面一定会混:

  1. 普通线性映射

    • 例如 Q=XWQQ = XW_QQ=XWQ
    • 它不一定是严格意义上的正交投影,只是工程里习惯叫 proj
  2. 维度变化意义上的升维/降维

    • 例如 up_proj 把维度从 dmodeld_{model}dmodel 提到 dffd_{ff}dffdown_proj 再降回去。
  3. 低秩约束

    • 例如 LoRA 用秩为 rrr 的分解限制更新矩阵的自由度。
  4. 命名陷阱(最容易误会的一点)

    • 很多实现里把“线性层”统一叫 *_proj,它可能只是在做 y = Wx,并不代表数学上严格的“投影算子”。

从名字上看都叫 projection,但它们在数学对象、作用时机和工程目的上都不同。一个简单的判断法是:遇到 proj,先问自己三件事。

  1. 这个 proj 在前向里出现吗?如果是,大概率就是线性映射(注意力/FFN/embedding 的某一段)
  2. 这个 proj 改变维度了吗?如果是,通常就是升维/降维结构(比如 up_proj/down_proj
  3. 这个 proj 的参数量是不是刻意受限(比如 rank 很小)?如果是,通常是在做低秩约束(比如 LoRA)

这三问不需要你懂任何高深数学,但能让你读代码时不至于把“线性映射”和“正交投影”混为一谈。

如果要再口语化一点,可以这么分:

  • Q/K/V/O:像同一个人换了四种工牌,身份没变,但职责不同
  • up_proj/down_proj:像先把桌子铺大再摆菜,摆完还得收回主桌面
  • LoRA:像不给整栋楼重装,只在关键几根水管上加一个小旁路

1.4 先给出全文结论

提前说结论,这样你后面看公式时会更有方向感:

  • Embedding 与特征扩张,解决的是如何把离散或纠缠的信息放进可学习的连续空间
  • 注意力投影,解决的是如何把同一个表示拆成“检索视角”“被检索视角”“内容视角”
  • FFN 的先升后降,解决的是如何在 token 内部构造更强的非线性特征组合能力
  • LoRA 的低秩分解,解决的是如何用极少的参数更新逼近有用的更新方向
  • LLaMA 的结构变体(例如门控 FFNGQA),解决的是在几乎不改变总框架的前提下,重新分配维度与计算预算

换句话说,升维/降维并不是一条孤立技巧,而是一整套关于表达与约束如何分配的设计哲学。

1.5 本章小结

这一章先把问题立住:训练中反复出现的升维、降维、projection,并不是重复造轮子,而是在不同层面处理同一个核心矛盾。从第 2 章起,补最少够用的线性代数工具,再逐步进入 TransformerLoRALLaMA


第 2 章 线性代数底座:升维、降维、秩与投影

本章给后文打数学地基:当你看到一个 W、一个 proj、一个 rank 时,能迅速判断它在“限制/扩展什么自由度”。本章会稍微偏数学,但只保留支撑后文所需的部分。

2.1 为什么神经网络几乎处处都是矩阵乘法

先从最小单元开始。把一层线性层写出来:

y=Wx+b
y = Wx + b
y=Wx+b

其中 x∈Rdinx in mathbb{R}^{d_{in}}xRdiny∈Rdouty in mathbb{R}^{d_{out}}yRdoutW∈Rdout×dinW in mathbb{R}^{d_{out} times d_{in}}WRdout×dinb∈Rdoutb in mathbb{R}^{d_{out}}bRdout

如果你更喜欢“看形状”而不是“看抽象定义”,那这一层其实就只在回答一个问题:

Rdin⟶Rdout
mathbb{R}^{d_{in}} longrightarrow mathbb{R}^{d_{out}}
RdinRdout

也就是说,输入原来活在一个 dind_{in}din 维空间里,经过 WWW 之后,被送进了另一个 doutd_{out}dout 维空间。

这件事有两个关键含义:

  1. “升维/降维”首先是形状变化。

    • 如果 dout>dind_{out} > d_{in}dout>din,你可以把它叫升维(更宽的输出空间)。
    • 如果 dout<dind_{out} < d_{in}dout<din,你可以把它叫降维(更窄的输出空间)。
  2. 线性层是在“换坐标系 + 重新组合”。

    • WWW 的每一行都是一个“从输入里抽取特征”的方向。
    • yiy_iyi 可以理解为把 xxx 投到第 iii 个方向上(严格说这是内积),再加偏置。

把它放回生活里,其实很像你拿一份简历去做不同维度的打分

  • 一个面试官更看重项目经验
  • 一个面试官更看重数学基础
  • 一个面试官更看重表达能力

同一份输入材料,经过不同“打分方向”之后,就会得到一组新的数值。矩阵 WWW 做的,本质上就是这种“按不同角度重新看一遍输入”的事。

你可能会问:既然线性层这么基础,为什么还要在模型里反复堆叠很多线性层?

因为单个线性层无论多宽,本质上还是线性变换。真正让模型变强的是“线性层 + 非线性 + 结构化组合”的反复堆叠:

  • Transformer 的注意力:用 Q/K/VQ/K/VQ/K/V 的线性映射,把“相似度计算”和“内容聚合”组织成一个可微的全局信息路由
  • FFN:用升维后的非线性,把 token 内部的特征组合能力推到更强

在工程上,你会看到我们很少显式写出 bbb。原因是:

  • 在许多 Transformer 实现里,某些 projection 层会省略 bias(或把 bias 吸收到其他层里),这不会改变我们在本文里讨论的“维度与自由度”逻辑。

2.2 线性映射不等于正交投影

这是第一个必须澄清的概念:工程里叫 proj,通常只是“线性层”;数学里叫“投影”,有更强的约束。

数学上,一个投影算子(projection operator) PPP 通常满足:

P2=P
P^2 = P
P2=P

这句话的意思是:投一次和投两次效果一样(已经落到子空间里了,再投也不会变)。

正交投影(orthogonal projection) 还额外满足对称性:

P⊤=P
P^top = P
P=P

对应的几何直觉是:xxx 被投影到某个子空间 Smathcal{S}S 上得到 PxPxPx,并且误差 x−Pxx – PxxPx 与子空间正交。

如果你学过最小二乘,其实这里有一个非常经典的公式。假设子空间 Smathcal{S}S 由矩阵 AAA 的列向量张成,那么投影到这个子空间的正交投影矩阵可以写成:

P=A(A⊤A)−1A⊤
P = A(A^top A)^{-1}A^top
P=A(AA)1A

于是任意向量 xxx 在这个子空间上的正交投影就是:

x^=Px=A(A⊤A)−1A⊤x
hat x = Px = A(A^top A)^{-1}A^top x
x=Px=A(AA)1Ax

这个公式背后的直觉非常生活化:像手电筒打影子。

  • 原始物体是 xxx
  • 墙面就是你允许保留的子空间 Smathcal{S}S
  • 投影后的影子是 x^hat xx

影子肯定丢失了一些三维信息,但在“墙面这个约束”下,它已经是距离原物体最近的表示了。

反过来,一般线性层 WWW

  • 既不要求 W2=WW^2 = WW2=W
  • 也不要求 W⊤=WW^top = WW=W

所以绝大多数 q_proj/k_proj/v_proj 在数学上不是“投影算子”,只是线性映射。

那为什么工程上还爱叫 proj

因为在模型结构里,它们经常承担“把一个表示映射到另一个用途/子空间”的角色,比如从 dmodeld_{model}dmodel 映射到每个 head 的 dkd_kdkdvd_vdv。这里的“子空间”更多是语义上的子空间(不同 head、不同角色),而不是线性代数里严格定义的一个固定子空间。

结论:看到 *_proj 时,不要自动脑补“正交投影”,先把它当作“线性层”,需要严格投影时我们会明确写出 P2=PP^2=PP2=P 这种性质。

2.3 秩是什么,为什么低秩代表受限自由度

矩阵的秩(rank)是理解 LoRA 的关键钥匙。

给定 W∈Rdout×dinW in mathbb{R}^{d_{out} times d_{in}}WRdout×din,它的秩 rank(W)mathrm{rank}(W)rank(W) 可以理解为:

  • WWW 的列向量中最多有多少个线性无关方向
  • 等价地:WWW 的行向量中最多有多少个线性无关方向
  • 更本质地:线性变换 x↦Wxx mapsto WxxWx 的像空间(输出可达子空间)的维度

把这个定义翻译成“升降维语境”的直觉就是:

就算 doutd_{out}dout 很大,如果 rank(W)=rmathrm{rank}(W)=rrank(W)=r 很小,那么所有输出 WxWxWx 都只能落在 Rdoutmathbb{R}^{d_{out}}Rdout 里的一个 rrr 维子空间里。

所以“输出空间大”不代表“有效自由度大”。参数矩阵看起来很大,也不代表它能沿很多独立方向变化。

当我们说“低秩约束”,本质上是在说:我们愿意把可训练自由度限制在 rrr 个主方向上,因为很多时候这已经足够拟合任务差异,同时还能显著节省参数与带来更稳定的训练。

如果你更习惯从分解角度理解,也可以把秩 rrr 的矩阵看成:

W=∑i=1raibi⊤
W = sum_{i=1}^{r} a_i b_i^top
W=i=1raibi

它最多只由 rrr 个“方向对”叠加而成。这意味着它不是不能表达变化,而是只能沿着少数几个主通道表达变化。

下一章我们会看到 LoRA 把更新写成 ΔW=BADelta W = BAΔW=BA,其中 A∈Rr×dinA in mathbb{R}^{r times d_{in}}ARr×dinB∈Rdout×rB in mathbb{R}^{d_{out} times r}BRdout×r,这直接保证了:

rank(ΔW)≤r
mathrm{rank}(Delta W) le r
rank(ΔW)r

这就是“用结构强行限制自由度”的典型例子。

2.4 奇异值分解与最优低秩近似

核心公式:

W=UΣV⊤
W = U Sigma V^top
W=UΣV

W≈UrΣrVr⊤
W approx U_r Sigma_r V_r^top
WUrΣrVr

这两行公式背后,是一个非常强的结论:SVD 不只是分解,它还给出了“什么是最重要方向”的排序方式。

2.4.1 SVD 的可读形式

W=UΣV⊤W = USigma V^topW=UΣV 写得更“可解释”一点:

  • U=[u1,…,udout]U = [u_1,dots,u_{d_{out}}]U=[u1,,udout] 是输出空间的一组正交基
  • V=[v1,…,vdin]V = [v_1,dots,v_{d_{in}}]V=[v1,,vdin] 是输入空间的一组正交基
  • Σ=diag(σ1,σ2,… )Sigma = mathrm{diag}(sigma_1,sigma_2,dots)Σ=diag(σ1,σ2,),其中 σ1≥σ2≥⋯≥0sigma_1 ge sigma_2 ge cdots ge 0σ1σ20

于是:

W=∑i=1min⁡(dout,din)σi uivi⊤
W = sum_{i=1}^{min(d_{out},d_{in})} sigma_i , u_i v_i^top
W=i=1min(dout,din)σiuivi

每一项 σiuivi⊤sigma_i u_i v_i^topσiuivi 都是一个秩为 1 的矩阵:它把输入沿 viv_ivi 的分量抽出来,映射到输出方向 uiu_iui 上,并用 σisigma_iσi 控制强度。

所以奇异值大小 σisigma_iσi 可以被看成“第 iii 个通道的重要程度”。

这时你可以把 SVD 想成把一大堆混杂的信息,拆成一层层“主成分包裹”

  • 最大的 σ1sigma_1σ1 对应最粗、最显眼的主方向
  • 后面的 σ2,σ3,…sigma_2,sigma_3,dotsσ2,σ3, 依次描述更细、更次要的变化

这很像你在收拾房间时先收“大件”,再收“小件”。先把最占空间、最影响视觉的东西归位,房间很快就“像样了”;后面那些零碎杂物当然也重要,但对整体轮廓的影响没那么大。

2.4.2 为什么截断 SVD 是“最优低秩近似”

如果我们只保留前 rrr 项:

Wr=∑i=1rσi uivi⊤
W_r = sum_{i=1}^{r} sigma_i , u_i v_i^top
Wr=i=1rσiuivi

那么 WrW_rWr 的秩不超过 rrr,并且它在一个很强的意义下是最好的:在所有秩不超过 rrr 的矩阵中,WrW_rWr 使得 WWW 的重构误差最小(Eckart-Young-Mirsky 定理)。

用 Frobenius 范数(也就是把所有元素平方加和开根)来写,就是:

Wr=arg⁡min⁡rank(X)≤r∥W−X∥F
W_r = argmin_{mathrm{rank}(X)le r} |W – X|_F
Wr=argrank(X)rminWXF

并且最小误差有闭式表达:

∥W−Wr∥F2=∑i=r+1min⁡(dout,din)σi2
|W – W_r|_F^2 = sum_{i=r+1}^{min(d_{out},d_{in})} sigma_i^2
WWrF2=i=r+1min(dout,din)σi2

这条公式给了你一个非常“工程友好”的直觉:

σisigma_iσi 衰减很快时,用很小的 rrr 就能保留大部分能量;这正是低秩方法在大模型里经常有效的原因之一。

你也可以把它理解成一种“先抓大头”的压缩逻辑:

  • 如果前几个奇异值已经占了绝大多数能量
  • 那么后面很长一串小奇异值虽然存在,但对整体结构的贡献有限

这正是很多压缩、降维、参数高效微调方法能成立的心理基础:不是所有方向都同样重要。

(严格证明通常利用范数的酉不变性与对角矩阵的最佳截断性质;此处直接使用结论,把它作为理解 LoRA rank 的核心数学支撑。)

2.4.3 这和 LoRA 有什么关系

LoRA 并不是直接对 WWW 做 SVD 截断,而是把“更新量”写成低秩形式:

W′=W+ΔW,ΔW=BA
W' = W + Delta W,quad Delta W = BA
W=W+ΔW,ΔW=BA

如果你把训练过程中真实需要的更新量(假想的全量微调更新)记为 ΔW⋆Delta W^starΔW,经验上它常常具有“能量集中在少数方向”的结构倾向。于是我们希望用一个 rank 很小的 ΔWDelta WΔW 去逼近它。

从生活里打个比方,这有点像你给老房子做翻修:

  • 全量微调像把整栋楼全部拆掉重装
  • LoRA 更像先判断“真正需要动的大改动其实集中在几条主干管线”
  • 于是你只在这些主干方向上加一个小型旁路系统

这就是为什么 LoRA 给人的感觉不是“我把模型能力砍掉了”,而更像“我假设真正有价值的改动集中在少数几个方向上”。

这就是为什么在很多任务上,rrr 取 8、16、32 这样的很小数值,也能达到很好的效果:你不是在把模型压成低秩,你是在把“允许更新的方向”压成低秩。

后面第 6 章会把这个想法用更工程化的语言解释清楚,并讨论 rrr 和效果/开销的关系。

2.5 本章小结

这一章我们建立了三个“后面会反复用到”的数学基石:

  1. 线性层 y=Wx+by=Wx+by=Wx+b 是所有升维/降维讨论的最小单元,升降维首先是矩阵形状变化
  2. 工程里叫 proj 的东西,大部分只是线性映射;数学上“投影算子”需要满足 P2=PP^2=PP2=P(正交投影还要 P⊤=PP^top=PP=P
  3. rank(W)mathrm{rank}(W)rank(W) 描述的是有效自由度;SVD 告诉我们“重要方向”如何排序,并给出最优低秩近似的严格结论

带着这三点进入后文,你会更容易把 FFN 的升维、Q/K/V 的投影、以及 LoRA rank 的低秩约束放到同一套语言里理解。


第 3 章 把学生信息变成向量:从离散特征到可训练 Embedding

回到高考志愿分流的例子:你之所以能“越看越清楚”,本质上是把离散信息(选科、标签、文本)变成了一个可学习的稠密空间表示 xxx

3.1 One-hot 为什么几乎不可训练地低效

先从最朴素的表示方式说起。假设我们要表示一个离散类别,比如学生的选科组合、意向专业类别、城市偏好标签,最直接的方法是 one-hot

如果一共有 VVV 个离散类别,那么第 iii 个类别可以写成:

ei=[0⋮1⋮0]∈RV
e_i =
begin{bmatrix}
0 \
vdots \
1 \
vdots \
end{bmatrix}
in mathbb{R}^{V}
ei=010RV

其中只有第 iii 维是 111,其他维度全是 000

这个表示有一个明显优点:干净、明确、不歧义。iii 类就是第 iii 类,谁也不会和谁混在一起。

但它的问题也一样明显:

  1. 维度很高,但信息非常稀。

    • 如果类别数 VVV 很大,向量长度就会非常长。
    • 但每次真正有用的只有一个位置。
  2. 类别之间没有“相似性结构”。

    • one-hot 里,“人工智能”和“计算机科学”一样远,“人工智能”和“临床医学”也一样远。
    • 这显然不符合真实世界的语义关系。
  3. 它几乎没法表达组合与迁移。

    • 一个模型如果只看 one-hot,就很难天然学到“物化生”和“物化地”在某些维度上更接近,而“物化生”和“史政地”更远。

这就是为什么 one-hot 虽然适合做“编号”,却不适合做“语义空间”。

one-hot 最大的问题不是“太简单”,而是只会区分类别,不会表达类别之间的关系

回到高考志愿分流的例子,这一点尤其明显。假设你只把“选科组合”“城市偏好”“预算区间”“职业兴趣”都当成 one-hot 标签扔进去,模型当然知道每个标签是谁,但它并不知道:

  • “更看重城市平台”和“愿意接受跨省”通常更接近
  • “物理强、数学强”和“适合理工”之间关系更近
  • “预算敏感”和“志愿风险偏好低”在很多场景下会一起出现

也就是说,one-hot 能告诉模型“这是什么”,却很难直接告诉模型“它和别的东西像不像、近不近、能不能迁移地组合起来”。

3.2 Embedding 本质上是在学一个可微的语义坐标系

要解决上面的问题,最自然的办法就是:不要让离散类别直接活在 VVV 维稀疏空间里,而是把它们映射到一个更紧凑、可学习的稠密空间中。

如果词表/类别表大小是 VVV,目标 embedding 维度是 dmodeld_{model}dmodel,那么 embedding 矩阵可以写成:

E∈RV×dmodel
E in mathbb{R}^{V times d_{model}}
ERV×dmodel

iii 个离散符号对应的向量就是矩阵的第 iii 行:

xi=Ei∈Rdmodel
x_i = E_i in mathbb{R}^{d_{model}}
xi=EiRdmodel

如果你坚持从矩阵乘法的角度看,它也可以写成:

xi=ei⊤E
x_i = e_i^top E
xi=eiE

其中 eie_iei 就是刚才那个 one-hot 向量。这个式子很值得停一下,因为它揭示了一个常被忽略的事实:

Embedding 并不神秘,它本质上就是“用 one-hot 去乘一个可训练矩阵,然后把对应行取出来”。

也就是说,Embedding 可以被看成一种查表版线性层

  • one-hot 负责告诉你“该查哪一行”
  • 矩阵 EEE 负责存储每个离散类别对应的稠密表示

真正重要的地方在于:矩阵 EEE 不是手工写死的,而是在训练过程中被不断更新的。也正因为如此,模型会逐渐把“经常在类似上下文里出现、承担类似功能、或在任务上有相似作用”的符号,推到更接近的位置上。

如果把这件事翻成更生活化的话,Embedding 就是在学一个可微的语义坐标系

  • 不是简单地给每个标签发一个编号
  • 而是给每个标签分配一组“连续坐标”
  • 这些坐标会随着任务目标不断被调整

在高考志愿场景里,这就意味着模型可以逐渐学到:

  • “偏理工”“数学强”“能接受跨省”在某些维度上更相关
  • “重视城市平台”“更看重资源”在另一些维度上会靠得更近
  • “预算受限”“风险偏好低”也可能形成另一组稳定的结构

你不需要显式告诉模型“这些特征彼此相似”,它会通过训练把这些关系编码进向量空间里。

3.3 高维稠密表示为什么比低维离散表示更有表达力

很多人第一次看到 Embedding 会有一个很自然的疑问:为什么非得映射到更高维、更稠密的空间里?低维一点不是更省吗?

答案不只是“空间更大”,更重要的是:高维空间允许不同属性被部分解耦。

举个非常贴近本文主线的理解方式。假设一个学生的向量表示是:

x∈Rdmodel
x in mathbb{R}^{d_{model}}
xRdmodel

如果 dmodeld_{model}dmodel 太小,模型就必须把很多信息硬挤在少数几个维度里:

  • 理科能力
  • 语言表达
  • 城市偏好
  • 预算敏感度
  • 风险偏好
  • 专业兴趣

这些因素会彼此缠在一起,后面的模型层很难把它们重新拆开。

但如果 dmodeld_{model}dmodel 足够大,模型就可以把这些属性分散到不同方向上,哪怕不是“一维对应一个概念”那么干净,至少也能做到部分解耦。一旦属性开始被分开,后面的层就更容易做三件事:

  1. 选择性关注某些因素
  2. 组合多个因素形成更复杂的判断
  3. 在不破坏整体结构的前提下调整局部方向

这也是为什么高维表示往往更适合做下游的注意力和 FFN 加工。你可以把它理解成:先把学生档案整理进一个更有层次的抽屉柜里,下游检索和组合才不会乱。

不过这里也要补一句:高维不等于万能。

  • 如果只是盲目把维度做大,而没有学到好的结构,那只是更贵的存储
  • 真正有价值的是“高维 + 稠密 + 可训练 + 能被任务目标不断调整”

所以第 3 章和第 1 章其实是连在一起的:第 1 章回答“为什么有时必须先升维”,第 3 章回答“升上去之后,模型到底把这些维度拿来干什么”。

3.4 用高考志愿分流的例子,把 Embedding 再落地一次

现在把上面的概念完整落回高考志愿场景。

一个学生的原始信息可能同时包含:

  • 离散标签:选科组合、省份、城市偏好、预算区间
  • 数值特征:数学成绩、语文成绩、总分、排名
  • 文本信息:自述兴趣、倾向专业、限制条件

在进入模型之前,这些信息不会直接拿原始格式硬拼。更常见的做法是:

  1. 离散字段先映射成 embedding 向量
  2. 连续字段先做数值变换或小线性层映射
  3. 文本字段用词向量/子词向量/编码器表示
  4. 再把这些部分合成为统一的学生表示 xxx

如果写得抽象一点,可以记成:

x=f(score,subjects,budget,city_pref,text)
x = f(text{score}, text{subjects}, text{budget}, text{city_pref}, text{text})
x=f(score,subjects,budget,city_pref,text)

这里的 f(⋅)f(cdot)f() 不一定是单一线性层,它可以是一串编码与融合操作。但无论实现细节怎样,目标都一样:

把原本异构、离散、难以直接比较的学生信息,映射到一个下游模型可以继续加工的统一向量空间里。

一旦有了这个统一表示 xxx,后面的注意力层才有东西可检索,FFN 才有东西可组合,LoRA 也才有可以被低秩调整的方向。

3.5 学生信息如何进入统一表示空间

学生信息进入统一表示空间的流程图

3.6 本章小结

这一章回答的是一个很基础但很关键的问题:后面所有“升维、降维、投影、注意力”,到底是建立在什么表示之上的?

结论可以压缩成三句:

  1. one-hot 适合做编号,但不适合承载语义结构
  2. Embedding 本质上是查表版线性层,用来学习一个可微的语义坐标系
  3. 高维稠密表示的真正价值,不只是空间更大,而是它让不同属性更容易被部分解耦、被检索、被重新组合

有了这一层统一表示 xxx,我们下一章才能自然进入 Q/K/V:既然已经把学生信息和问题都放进了向量空间,接下来模型是怎样从案例库里“找依据”的?


第 4 章 提问-检索-汇总:用 Q/K/V 从案例库里“找依据”

回到高考志愿分流的例子:你在回答一个具体学生的问题时,更像是在检索一堆历史案例/知识条目并加权汇总,而不是凭空拍脑袋。

4.1 同一个向量为什么不能既当问题又当答案

先从直觉说起。假设你现在面前有一个具体问题:

这个学生理科强,预算一般,希望尽量留在省内,那他的志愿应该怎么填?

如果你真的是一位经验丰富的老师,你脑子里通常会同时做三件事:

  1. 把这个问题整理成“我要找什么”的样子
  2. 在历史案例、规则经验、学校信息里搜索“谁和它最像”
  3. 把搜到的相关信息汇总起来,形成建议

问题在于,这三件事虽然围绕同一个语义对象展开,但它们扮演的角色并不一样。

  • “我要找什么”是查询视角
  • “谁能和它匹配”是索引视角
  • “匹配到之后真正要拿来用的内容”是内容视角

这就是为什么同一个表示不能简单地“一把梭”。

如果你把同一个向量同时拿来做“提问”和“答案”,模型就很难学到一种稳定的分工:哪些维度更适合做相似度比较,哪些维度更适合承载被聚合的内容。于是 Transformer 做了一个很关键的设计:同一个输入表示,先经过不同的线性映射,再分别承担不同角色。

用高考志愿场景来翻译:

  • Q:当前这个问题,到底想找什么样的依据
  • K:案例库里每条信息,如何把自己变成“方便被匹配”的索引卡片
  • V:案例库里每条信息,真正值得被读出来、被汇总的内容

这和图书馆找书非常像:

  • 你脑子里的需求是 Q
  • 书目索引卡是 K
  • 书的正文内容是 V

你不会拿整本书正文直接去做索引检索;同样,模型也不会直接拿“原始表示”同时兼任所有角色。

Q/K/V 的关键不是“多了三个矩阵”,而是把提问、匹配、取内容这三件事拆开学

4.2 注意力的核心公式

Q=XWQ,K=XWK,V=XWV
Q = XW_Q, quad K = XW_K, quad V = XW_V
Q=XWQ,K=XWK,V=XWV

Attention(Q,K,V)=softmax(QK⊤dk)V
text{Attention}(Q,K,V) = text{softmax}left(frac{QK^top}{sqrt{d_k}}right)V
Attention(Q,K,V)=softmax(dkQK)V

现在正式来看公式。

假设输入矩阵是:

X∈Rn×dmodel
X in mathbb{R}^{n times d_{model}}
XRn×dmodel

其中 nnn 是序列长度,dmodeld_{model}dmodel 是隐藏维度。通过三组不同参数矩阵,我们得到:

Q=XWQ,K=XWK,V=XWV
Q = XW_Q,qquad K = XW_K,qquad V = XW_V
Q=XWQ,K=XWK,V=XWV

其中常见形状是:

WQ∈Rdmodel×dk,WK∈Rdmodel×dk,WV∈Rdmodel×dv
W_Q in mathbb{R}^{d_{model}times d_k},quad
W_K in mathbb{R}^{d_{model}times d_k},quad
W_V in mathbb{R}^{d_{model}times d_v}
WQRdmodel×dk,WKRdmodel×dk,WVRdmodel×dv

所以:

Q∈Rn×dk,K∈Rn×dk,V∈Rn×dv
Q in mathbb{R}^{ntimes d_k},quad
K in mathbb{R}^{ntimes d_k},quad
V in mathbb{R}^{ntimes d_v}
QRn×dk,KRn×dk,VRn×dv

接下来最关键的一步是:

QK⊤∈Rn×n
QK^top in mathbb{R}^{ntimes n}
QKRn×n

这个矩阵的第 (i,j)(i,j)(i,j) 个元素,本质上就是“第 iii 个查询和第 jjj 个键有多匹配”。

如果回到高考志愿的类比里,可以这样看:

  • iii 行对应“当前要回答的某个局部问题”
  • jjj 列对应“案例库/上下文里的第 jjj 条候选依据”
  • 分数越高,说明这条候选依据越值得参考

为什么还要除以 dksqrt{d_k}dk

QK⊤dk
frac{QK^top}{sqrt{d_k}}
dkQK

因为当维度 dkd_kdk 变大时,内积的数值会自然变大,softmax 容易变得过于尖锐,导致训练不稳定。除以 dksqrt{d_k}dk 相当于做一个尺度校正,让分数保持在更合适的范围。

然后通过 softmax:

A=softmax(QK⊤dk)
A = text{softmax}left(frac{QK^top}{sqrt{d_k}}right)
A=softmax(dkQK)

得到注意力权重矩阵 AAA。这里每一行都会被归一化成一组权重,总和为 111。这意味着:

  • 它不是在做“硬检索”
  • 而是在做“软检索”
  • 也就是:不是只选一个最相关对象,而是允许多个候选条目按权重共同贡献

最后:

Attention(Q,K,V)=AV
text{Attention}(Q,K,V) = AV
Attention(Q,K,V)=AV

这一步表示:用刚刚算出的匹配权重,去对内容向量 VVV 做加权汇总。

这正是“提问-检索-汇总”的三步闭环:

  1. Q 决定我在找什么
  2. K 决定我如何判断“谁和问题相关”
  3. V 决定我最终把什么内容拿回来

如果用一句话总结这一整套机制:

注意力不是“让所有信息互相看看”,而是“让当前问题有能力从一堆候选信息里按相关性挑出依据并汇总”。

再把它压回到单个查询的形式,假设只有一个查询向量 q′q'q,以及一组候选键值对 {(ki,vi)}i=1N{(k_i,v_i)}_{i=1}^{N}{(ki,vi)}i=1N,那么它就写成:

αi=softmax(q′ki⊤dk),out=∑i=1Nαivi
alpha_i = text{softmax}left(frac{q'k_i^top}{sqrt{d_k}}right),qquad
text{out} = sum_{i=1}^{N}alpha_i v_i
αi=softmax(dkqki),out=i=1Nαivi

这正好和第 1 章里那个“当前学生的问题去检索案例库”的贯穿例子一一对应。

4.3 多头注意力为什么本质上也是一种“子空间分工”

如果单头注意力已经能做“提问-检索-汇总”,为什么还要多头?

最直观的原因是:一个问题往往不止一个检索角度。

还拿高考志愿的例子说,同一个学生的问题,可能同时需要看:

  • 成绩结构像不像
  • 城市偏好像不像
  • 风险承受能力像不像
  • 专业兴趣像不像

如果只让一个头去学所有关系,它很容易把这些不同维度的判断揉在一起。多头注意力的做法是:把表示拆成多个子空间,让不同的头各自学习不同的匹配模式。

形式上,假设有 hhh 个头,每个头都有自己的投影矩阵:

Q(m)=XWQ(m),K(m)=XWK(m),V(m)=XWV(m),m=1,…,h
Q^{(m)} = XW_Q^{(m)},quad
K^{(m)} = XW_K^{(m)},quad
V^{(m)} = XW_V^{(m)},qquad m=1,dots,h
Q(m)=XWQ(m),K(m)=XWK(m),V(m)=XWV(m),m=1,,h

每个头各自计算:

head(m)=softmax(Q(m)K(m)⊤dk)V(m)
text{head}^{(m)} = text{softmax}left(frac{Q^{(m)}{K^{(m)}}^top}{sqrt{d_k}}right)V^{(m)}
head(m)=softmax(dkQ(m)K(m))V(m)

最后再把多个头拼接起来并映射回主空间:

MultiHead(X)=Concat(head(1),…,head(h))WO
text{MultiHead}(X) = text{Concat}big(text{head}^{(1)},dots,text{head}^{(h)}big)W_O
MultiHead(X)=Concat(head(1),,head(h))WO

这里最值得记住的,不是公式本身,而是它背后的结构直觉:

多头注意力不是简单地“多算几次”,而是在说:同一个输入表示,应该允许多个检索小组并行工作,每个小组从自己的角度找依据。

如果用一个更接地气的类比,它像是一个志愿咨询专案组:

  • 一组老师重点看分数与选科
  • 一组老师重点看城市与地域偏好
  • 一组老师重点看预算与风险偏好
  • 一组老师重点看专业兴趣与长期规划

最后把这些视角的结果汇总起来,形成一个更稳的建议。

这也是为什么多头注意力常常比单头更强:它不是单纯增加参数,而是在强行制造“子空间分工”

4.4 原论文图

下面图直接来自 Attention Is All You Need 原论文

transformer_paper_fig1_architecture

4.5 本章小结

这一章真正想讲透的,其实只有一句话:

注意力机制的本质,不是“大家互相看一眼”,而是“当前问题如何从候选信息里检索依据,再把依据汇总回来”。

从这个角度看:

  1. Q 负责定义“我要找什么”
  2. K 负责定义“谁适合被匹配”
  3. V 负责定义“匹配上以后拿回什么内容”
  4. 多头注意力负责让不同子空间并行承担不同检索视角

有了这一层理解,你再回头看 Q/K/V/O 这些名字,就不会觉得它们只是一些莫名其妙的 proj 层,而会知道:它们是在把“提问-检索-汇总”这件事拆成可学习的模块。

既然模型已经能检索依据,下一步自然就是:拿回来的这些信息,怎样在 token 内部继续被重新组合? 这就会把我们带到下一章的 FFN


第 5 章 自动造交叉特征:FFN 的先升维再降维

5.1 如果不升维,只在原空间里做非线性会怎样

第 4 章的注意力更像“去案例库里找依据”。但真正给出建议时,你还需要做一件更“本地”的事:把这些依据和当前学生的特征,在一个 token 的表示里重新组合,产出更像“结论”的表示。

Transformer 里,承担这个角色的就是 FFN(也常叫 MLP)。它的一个关键特点是:逐 token 处理

  • 注意力:负责跨 token 检索与汇总(把“外部信息”拿回来)
  • FFN:负责 token 内部的非线性组合(把“拿回来的信息”做出更高阶的结构)

现在回答你问的“如果不升维会怎样”:

如果我们把 FFN 简化到极致,只允许在同一维度里做一次线性变换(哪怕后面接激活),它能做的组合其实非常受限。更具体一点:

  • 单纯的线性层只能学到“加权相加”的结构
  • 而很多关键判断需要的是“组合关系”(交叉特征),例如:
    • “理科强 并且 愿意跨省”通常导向不同建议
    • “城市偏好强 且 预算敏感”会改变学校范围

这类“并且/交互”关系,恰好就是第 1 章 XOR 例子在玩具数据上的抽象:单靠一条直线(线性层)切不漂亮

所以 FFN 的核心目标可以粗暴总结为一句:

把原空间里“分不开/组合不出来”的结构,搬到一个更宽的中间空间里,用非线性把它们拼出来,再压回主干表示。

5.2 FFN 的标准公式

FFN(x)=W2σ(W1x+b1)+b2
text{FFN}(x) = W_2 sigma(W_1 x + b_1) + b_2
FFN(x)=W2σ(W1x+b1)+b2

其中:

  • W1∈Rdff×dmodelW_1 in mathbb{R}^{d_{ff} times d_{model}}W1Rdff×dmodel
  • W2∈Rdmodel×dffW_2 in mathbb{R}^{d_{model} times d_{ff}}W2Rdmodel×dff
  • 通常 dff>dmodeld_{ff} > d_{model}dff>dmodel

把这个式子按“升维-非线性-降维”拆开看:

  1. 升维h=W1x+b1h = W_1x + b_1h=W1x+b1,把 dmodeld_{model}dmodel 维送到更宽的 dffd_{ff}dff
  2. 非线性h~=σ(h)tilde h = sigma(h)h~=σ(h),在更宽空间里做非线性变换
  3. 降维y=W2h~+b2y = W_2tilde h + b_2y=W2h~+b2,把结果压回 dmodeld_{model}dmodel

注意:这里的“升维”不是为了让最终输出维度变大(最后还是回到 dmodeld_{model}dmodel),而是为了让中间层有足够的“工作台面积”,去制造更丰富的交叉/组合特征。

在很多实现里,dffd_{ff}dff 常取 4dmodel4d_{model}4dmodel(例如 dmodel=4096d_{model}=4096dmodel=4096dff=11008d_{ff}=11008dff=11008 或附近),这不是拍脑袋:它基本反映了“想要足够的非线性组合能力”与“算力/显存预算”之间的折中。

5.3 一个直觉:先把菜铺开,再重新组合

回到你熟悉的“志愿建议”场景。注意力解决的是“去哪里找依据”;但依据拿回来以后,还需要把它们变成一句可执行建议,这一步更像是在厨房里做菜:

  • 你把食材(特征、证据)一股脑堆在案板上,信息都在,但很难直接出菜
  • 先把食材切开、分类、摆盘(升维),让不同因素不再纠缠
  • 然后按规则加热、混合、调味(非线性),做出“组合关系”
  • 最后装盘上桌(降维),变回一个简洁的输出表示

所以 FFNTransformer Block 里常被称作“逐 token 的非线性计算”,它不像注意力那样跨 token 交流,而更像“在每个 token 自己的脑子里做推理”。

5.4 从表达能力角度看“升维 -> 非线性 -> 降维”

这一小节我们把第 1 章的 XOR 例子正式接回来:FFN 为什么能“自动造交叉特征”?

关键点是:两层线性 + 非线性的组合,能表达很多“非线性可分”的结构。最直观的展示方式,就是写一个能精确实现 XOR 的小 FFN

5.4.1 用一个两层 FFN 精确实现 XOR(可计算版)

我们仍然用第 1 章的输入:

x=[x1x2],(x1,x2)∈{0,1}2
x=
begin{bmatrix}
x_1\
x_2
end{bmatrix},
quad (x_1,x_2)in{0,1}^2
x=[x1x2],(x1,x2){0,1}2

目标输出是:

y=x1⊕x2
y = x_1 oplus x_2
y=x1x2

在第 1 章我们证明过:不存在一组 (a,b,c)(a,b,c)(a,b,c) 使得 sign(ax1+bx2+c)text{sign}(ax_1+bx_2+c)sign(ax1+bx2+c) 在二维里把 XOR 分开。

现在我们用一个非常小的 FFN,取激活函数为 ReLU:σ(t)=max⁡(0,t)sigma(t)=max(0,t)σ(t)=max(0,t),并设:

W1=[1−1−11],b1=[00]
W_1 =
begin{bmatrix}
1 & -1\
-1 & 1
end{bmatrix},
quad b_1 =
begin{bmatrix}
0\
end{bmatrix}
W1=[1111],b1=[00]

W2=[11],b2=0
W_2 =
begin{bmatrix}
1 & 1
end{bmatrix},
quad b_2 = 0
W2=[11],b2=0

于是:

h=W1x,h~=σ(h),y=W2h~
h = W_1x,quad
tilde h = sigma(h),quad
y = W_2tilde h
h=W1x,h~=σ(h),y=W2h~

h~tilde hh~ 写开,其实就是两条“自动造出来的交叉特征”:

h~1=max⁡(0,x1−x2),h~2=max⁡(0,x2−x1)
tilde h_1 = max(0, x_1-x_2),qquad
tilde h_2 = max(0, x_2-x_1)
h~1=max(0,x1x2),h~2=max(0,x2x1)

最后输出:

y=h~1+h~2=∣x1−x2∣
y = tilde h_1 + tilde h_2 = |x_1-x_2|
y=h~1+h~2=x1x2

而当 (x1,x2)∈{0,1}2(x_1,x_2)in{0,1}^2(x1,x2){0,1}2 时,∣x1−x2∣|x_1-x_2|x1x2 恰好就是 XOR 的标签(相等为 0,不等为 1)。

这个构造很重要,因为它说明:

  1. FFN 不需要显式写出 x1x2x_1x_2x1x2 这种乘法特征,也能通过“升维 + ReLU”造出一个可分的中间表示
  2. 中间层的维度(这里是 2)就是 d_{ff} 的雏形:你给它多少宽度,就给它多少“造特征的工位”
5.4.2 回到 Transformer:d_{ff} 就是“造交叉特征的预算”

在真实的 Transformer 里,xxx 不是二维,而是 dmodeld_{model}dmodel 维;要处理的“交叉关系”也远比 XOR 复杂。

因此 FFN 的直觉就是:

  • W1W_1W1 把信息投到更宽空间:把潜在的组合关系“展开”
  • σsigmaσ 在宽空间里把空间切成更多区域(分段线性/门控)
  • W2W_2W2 再把这些非线性结果压回主干表示

你可以把它理解成:注意力负责“找信息”,FFN 负责“把信息组合成结构”,而“升维”提供的是组合结构所需的容量。

5.5 LLaMA 中的 gate_proj / up_proj / down_proj

LLaMAFFN 看起来“更显眼”,主要是因为它把 FFN 写成了门控结构(常见是 SwiGLU)。你会看到三个名字:

  • up_proj:升维分支之一
  • gate_proj:升维分支之二(用于门控)
  • down_proj:降维,把结果压回 dmodeld_{model}dmodel

一种常见写法是:

FFN(x)=Wdown(σ(Wgatex)⊙(Wupx))
text{FFN}(x)=W_{text{down}}Big(sigma(W_{text{gate}}x)odot (W_{text{up}}x)Big)
FFN(x)=Wdown(σ(Wgatex)(Wupx))

这里的 ⊙odot 是逐元素乘法。门控的直觉是:它让模型不仅能“生成一堆候选特征”(WupxW_{text{up}}xWupx),还能同时“决定哪些特征该放行”(σ(Wgatex)sigma(W_{text{gate}}x)σ(Wgatex))。

如果回到高考志愿建议的类比,门控像是:

  • 你能想到很多因素(候选特征)
  • 但你会根据当前问题,把其中一部分因素压下去,把另一部分因素抬上来

这类门控结构在实践里往往更强,也更稳定,因此 LLaMA 这类模型会让 FFN 变得更大、也更关键。

5.6 FFN 流程图

FFN 升维、非线性处理与降维的流程图

5.7 本章小结

这一章把“升维-非线性-降维”这条主线落在了 FFN 上,并把 XOR 例子接回来了:

  1. 注意力解决的是“检索依据”,FFN 解决的是“在 token 内部把依据组合成更高阶结构”
  2. FFNdff>dmodeld_{ff}>d_{model}dff>dmodel 不是为了输出更大维度,而是提供“造交叉特征的预算”
  3. XOR 这个玩具例子说明:只要有升维和非线性,一个非常小的 FFN 就能把二维线性不可分的问题变成可分
  4. LLaMA 的门控 FFNgate_proj/up_proj/down_proj)进一步增强了“生成特征 + 选择放行”的能力

下一章我们会把视角从“表征空间里的升降维”切到“参数更新空间里的低秩约束”:当建议口径变了,为什么 LoRA 能用很小的 rank 快速适配?


第 6 章 当口径变了怎么快速适配:LoRA 的低秩更新

继续沿用高考志愿分流的例子:当“建议口径”发生变化(更看重城市/就业/专业稳定性),你不想把整个模型重练一遍,而是希望用很小的可训练自由度快速把关键方向挪一挪。

6.1 全量微调的问题不只是显存大

先说清一个常见误解:大家提 LoRA 经常第一反应是“省显存”。显存当然重要,但如果你把它放到“高考志愿建议”这种长期运营的场景里,会发现更关键的问题其实是可控性和可维护性

当“建议口径”变化时(例如更看重城市平台/就业导向/专业确定性),你通常希望做到的是:

  • 只改动少数关键偏好,让建议风格可切换
  • 不破坏底座已经学到的通用能力(语言、推理、检索与组合结构)
  • 多套口径能并行存在、随时切换、易于部署

如果用全量微调(更新所有参数)来应对,会带来一串工程现实:

  1. 版本和部署成本高

    • 每个口径一套完整模型,权重大、发布慢、灰度复杂
  2. 容易“牵一发而动全身”

    • 你只想调某些偏好,但全量更新会在很多层同时变化,稳定性难控
  3. 多口径切换更笨重

    • 你想“一个底座 + 多个适配器”,但全量微调等价于“多个大模型”
  4. 数据与训练成本更高

    • 全量微调更依赖数据规模与正则化,否则更容易退化

所以这章的目标可以压缩成一句话:

冻结一套强底座,只允许你用很小的“改动预算”,把模型沿少数关键方向挪一下。

6.2 LoRA 的核心公式

W′=W+ΔW,ΔW=BA
W' = W + Delta W, quad Delta W = BA
W=W+ΔW,ΔW=BA

其中:

  • A∈Rr×dinA in mathbb{R}^{r times d_{in}}ARr×din
  • B∈Rdout×rB in mathbb{R}^{d_{out} times r}BRdout×r
  • r≪min⁡(din,dout)r ll min(d_{in}, d_{out})rmin(din,dout)

这行式子最关键的性质是:更新量天然是低秩的。

rank(ΔW)=rank(BA)≤r
mathrm{rank}(Delta W) = mathrm{rank}(BA) le r
rank(ΔW)=rank(BA)r

也就是说:你不是在“压缩权重矩阵 WWW”,你是在限制更新空间,只允许它在少数 rrr 个主方向上改变模型。

LoRA 最值得记住的不是“省参数”,而是:它把更新限制成“少数几个主方向上的改动”。

工程实现里经常还会写成:

W′=W+αrBA
W' = W + frac{alpha}{r}BA
W=W+rαBA

其中 αalphaα(也就是 lora_alpha)更像一个“旁路增益旋钮”。加上 α/ralpha/rα/r 的缩放后,不同 rrr 下的更新幅度更容易保持在同一量级,调参也更直观。

6.3 为什么这是“先降维再升维”

ΔW=BADelta W=BAΔW=BA 写成作用在输入上的形式更容易理解:

y=W′x=Wx+ΔWx=Wx+BAx
y = W'x = Wx + Delta W x = Wx + BAx
y=Wx=Wx+ΔWx=Wx+BAx

这条计算路径本身就是“先降维再升维”:

  1. A∈Rr×dinA in mathbb{R}^{rtimes d_{in}}ARr×din:把 xxx 压到 rrr 维瓶颈空间(降维)
  2. B∈Rdout×rB in mathbb{R}^{d_{out}times r}BRdout×r:再把瓶颈空间抬回输出空间(升维)

但这里的“降/升维”一定要读对:它不是第 5 章那种“表征空间为了造交叉特征而升维”,而是:

用一个 rrr 维瓶颈,限制“允许更新的自由度”。

因此 LoRA 的直觉不是“模型前向变简单了”,而是“训练时你给更新开了一个小旁路,并规定这条旁路只有 rrr 个自由方向”。

这也解释了它的参数效率:

  • 全量更新:dout⋅dind_{out}cdot d_{in}doutdin
  • LoRAr(din+dout)r(d_{in}+d_{out})r(din+dout)

r≪min⁡(din,dout)r ll min(d_{in},d_{out})rmin(din,dout) 时,差距会非常大。

6.4 用 SVD 视角理解 LoRA

第 2 章我们讲过一个核心事实:SVD 给出了“最优低秩近似”的意义,截断前 rrr 项可以保留主要能量。

把这个直觉搬到 LoRA 上,你可以把它理解成对更新量 ΔW⋆Delta W^starΔW 的一种假设:

有效的更新方向往往集中在少数几个主方向上,因此 ΔW⋆Delta W^starΔW 可以被一个低秩矩阵很好地逼近。

如果我们真的对 ΔW⋆Delta W^starΔW 做 SVD:

ΔW⋆≈UrΣrVr⊤
Delta W^star approx U_r Sigma_r V_r^top
ΔWUrΣrVr

那你会发现它天生就可以写成 LoRA 形式:

ΔW⋆≈(UrΣr)(Vr⊤)
Delta W^star approx (U_rSigma_r)(V_r^top)
ΔW(UrΣr)(Vr)

令:

B=UrΣr,A=Vr⊤
B = U_rSigma_r,qquad A = V_r^top
B=UrΣr,A=Vr

就得到 ΔW≈BADelta W approx BAΔWBA,并且 rank(ΔW)≤rmathrm{rank}(Delta W)le rrank(ΔW)r

这里有一个非常关键但容易被忽略的点:

  • SVD 截断是在“给定矩阵后求最佳秩 rrr 逼近”
  • LoRA 是在“训练时把更新空间限制在秩 rrr 的集合里”

也就是说,LoRA 不需要你真的去算 SVD,它只是利用了“低秩结构常常够用”的事实,把这个结构当作可控的归纳偏置塞进训练过程里。

因此 lora_rankrrr)在直觉上就是:

我愿意给“更新空间”多少个主方向?

rrr 太小,口径变化装不下;当 rrr 太大,参数与不稳定性上来,甚至更接近全量微调。

6.5 lora_ranklora_alphalora_target 分别在控制什么

把前面第 2 章(秩/SVD)和第 5 章(FFN 造交叉特征)连起来后,这几个超参就很好解释了:

  1. lora_rankrrr

    • 控制低秩更新的秩上限:rank(ΔW)≤rmathrm{rank}(Delta W)le rrank(ΔW)r
    • 更像“改动预算”的维度数:允许沿多少个主方向去改模型
  2. lora_alphaαalphaα

    • 控制旁路更新的有效幅度(常见是 α/ralpha/rα/r 的缩放)
    • 更像“这条旁路的增益旋钮”,影响适配强度
  3. lora_target

    • 控制把 LoRA 插到哪些线性层上,也就是你想改“哪一类行为”

结合前面高考志愿的叙事,你可以用一个很工程化的对照来理解 lora_target

  • 插在注意力投影(Q/K/V/O)上:更像在改“检索偏好/检索视角”,例如你更愿意参考哪类案例、哪类依据
  • 插在 FFNup/downgate/up/down)上:更像在改“如何把依据组合成结论”,例如你更看重哪类交叉关系

实践里常见做法是两者都插,但如果你希望训练更稳、可解释性更强,可以先从一个侧重点开始,再逐步扩大 target 范围。

6.6 原论文图

下面这张图直接来自 LoRA 原论文。它最直观地说明了:LoRA 不是改写整层权重,而是在冻结原权重的同时,加上一条低秩旁路。

image-20260421174452822

6.7 LoRA 低秩旁路图

LoRA 低秩旁路更新的流程图

6.8 本章小结

这一章把 LoRA 的低秩约束和前面的 SVD/FFN 主线串起来了:

  1. 全量微调的问题不只是显存,而是可维护性、可控性与多口径切换成本
  2. LoRAΔW=BADelta W=BAΔW=BA 把更新限制在低秩集合里:rank(ΔW)≤rmathrm{rank}(Delta W)le rrank(ΔW)r
  3. 用第 2 章的 SVD 直觉理解:很多有效更新可以由少数主方向逼近,rrr 就是你愿意放行的“更新自由度”
  4. 和第 5 章对照:dffd_{ff}dff 是“造交叉特征的预算”,rrr 是“改变模型行为的预算”

下一章我们会继续看结构层面的延伸:为什么 LLaMA 的门控 FFN 和头结构会让“升维/降维”在工程上更显眼。


第 7 章 LLaMA 为什么让“升维/降维”更显眼

这一章把视角从“标准 Transformer”延伸到 LLaMA 系列,解释它在几乎不改大框架的前提下,是怎么重排维度与计算预算的。

7.1 gate_proj / up_proj / down_proj 本质上还是 FFN

先把第 5 章和这一章的分工说清楚:

  • 第 5 章讲的是:为什么 FFN 需要“升维 -> 非线性 -> 降维”
  • 第 7 章讲的是:LLaMA 怎样把这件事做得更显式、更大、更讲究预算

所以看到 gate_proj / up_proj / down_proj 时,不要被名字吓住。它们并不是一种全新的魔法结构,本质上还是第 5 章那类“在表征空间里先展开、再组合、再压回去”的 FFN

只不过 LLaMA 把标准两层 FFN

FFN(x)=W2σ(W1x+b1)+b2
text{FFN}(x)=W_2sigma(W_1x+b_1)+b_2
FFN(x)=W2σ(W1x+b1)+b2

改写成了更强的门控形式。于是你在代码里不再只看到“一个升维矩阵 + 一个降维矩阵”,而会看到:

  • up_proj:负责生成候选特征
  • gate_proj:负责生成门控信号
  • down_proj:把门控后的结果压回主干空间

这就是为什么 LLaMA 看起来“到处都是 proj”。不是因为它比 Transformer 多了完全不同的数学对象,而是因为它把内部结构拆得更明确了。

7.2 SwiGLU 的公式与维度形状

LLaMA 常见的门控 FFN 写法可以记成:

FFN(x)=Wdown(σ(Wgatex)⊙(Wupx))
text{FFN}(x)=W_{text{down}}Big(sigma(W_{text{gate}}x)odot (W_{text{up}}x)Big)
FFN(x)=Wdown(σ(Wgatex)(Wupx))

其中常见形状是:

Wgate∈Rdff×dmodel,Wup∈Rdff×dmodel,Wdown∈Rdmodel×dff
W_{text{gate}} in mathbb{R}^{d_{ff}times d_{model}},quad
W_{text{up}} in mathbb{R}^{d_{ff}times d_{model}},quad
W_{text{down}} in mathbb{R}^{d_{model}times d_{ff}}
WgateRdff×dmodel,WupRdff×dmodel,WdownRdmodel×dff

把计算拆开看更容易理解:

g=Wgatex,u=Wupx
g = W_{text{gate}}x,qquad
u = W_{text{up}}x
g=Wgatex,u=Wupx

h~=σ(g)⊙u
tilde h = sigma(g)odot u
h~=σ(g)u

y=Wdownh~
y = W_{text{down}}tilde h
y=Wdownh~

这里最值得注意的是:LLaMA 不是只做一次升维,而是做了两条并行的升维分支

  1. 一条分支生成候选特征 uuu
  2. 一条分支生成门控信号 σ(g)sigma(g)σ(g)
  3. 两者逐元素相乘,再决定哪些特征被放大、哪些被压制

所以它比普通 FFN 多出来的,不只是“多一个矩阵”,而是多了一层更细的结构控制:

标准 FFN 更像“先做出一堆特征,再整体映射回来”;SwiGLU 更像“先做出一堆候选特征,再用另一条支路决定哪些特征该被放行”。

如果你回到高考志愿建议的类比,它就像:

  • up_proj:把你脑子里所有可能相关的因素都摊开
  • gate_proj:根据当前问题,判断这些因素哪些该重、哪些该轻
  • down_proj:把筛过之后的结果重新整理成一句简洁的建议

这也是为什么 LLaMAFFN 在实践里往往更强:它不是只靠“更宽”,而是在“更宽”的同时引入了更细的门控。

7.3 GQA:投影矩阵的“头数结构”变了,但维度逻辑没变

除了 FFNLLaMA 里另一个很值得讲清楚的点是 GQA(Grouped-Query Attention)。

先回顾标准多头注意力。假设有 hhh 个头,那么通常:

  • Qhhh 个头
  • Khhh 个头
  • V 也有 hhh 个头

这很好理解,但代价是:推理时每个 token 都要缓存所有头的 K/V,KV cache 会很大。

GQA 的核心思路是:保留较多的查询头,但减少 K/V 头数。

如果设:

  • 查询头数为 hqh_qhq
  • KV 头数为 hkvh_{kv}hkv
  • hkv<hqh_{kv} < h_qhkv<hq

那么多个查询头会共享同一组 K/V 头。直觉上,你可以把它理解成:

  • 问问题的人还是很多(不同查询视角仍然保留)
  • 但资料管理员的人数减少了(更少的 K/V 头被共享)

为什么这件事重要?因为它直接影响推理时的计算和内存预算。

如果单头维度是 dheadd_{head}dhead,序列长度是 TTT,那么 KV cache 大小大致与:

T⋅hkv⋅dhead
T cdot h_{kv} cdot d_{head}
Thkvdhead

成正比,而不是和 hqh_qhq 成正比。也就是说:你把 KV 头数降下来,KV cache 就会明显变小。

这就是 GQA 的工程核心:

尽量保留足够多的“提问视角”,同时减少“被缓存的答案索引数量”。

所以 GQA 的本质并不是“改了注意力的数学定义”,而是重分配了头结构里的预算

  • 更多预算留给 Q 的多样性
  • 更少预算花在 K/V 的存储与重复上

这和本文的总主题完全一致:不是一味地增大所有维度,而是把预算放在最值钱的地方。

7.4 RoPE 与 “旋转”不是升降维,但会改变你如何理解 Q/K

RoPE(Rotary Positional Embedding)是这一章里最容易被误读的一点,因为它会让人觉得:“既然也在改向量,那是不是又是一种升维/降维?”

答案是否定的。RoPE 的关键不是改变维度,而是在保持维度不变的前提下,把位置信息以旋转的方式注入到 Q/K

如果把二维子空间里的一对分量写成:

[q2iq2i+1]
begin{bmatrix}
q_{2i}\
q_{2i+1}
end{bmatrix}
[q2iq2i+1]

那么 RoPE 对它做的是一个位置相关的旋转:

R(θp)[q2iq2i+1]
R(theta_p)
begin{bmatrix}
q_{2i}\
q_{2i+1}
end{bmatrix}
R(θp)[q2iq2i+1]

其中:

R(θp)=[cos⁡θp−sin⁡θpsin⁡θpcos⁡θp]
R(theta_p)=
begin{bmatrix}
costheta_p & -sintheta_p\
sintheta_p & costheta_p
end{bmatrix}
R(θp)=[cosθpsinθpsinθpcosθp]

这里的 θptheta_pθp 与位置 ppp 有关。K 也会做对应的旋转。这样做的效果是:内积 QK⊤QK^topQK 不再只反映内容相似,还会隐式带上相对位置信息。

所以 RoPE 的作用更像是:

  • 不改“空间有多大”
  • 而是改“你在这个空间里怎么看方向关系”

这和前面讲的升维/降维不是同一类操作,但它仍然值得放在这一章讲,因为它体现了 LLaMA 的另一种设计哲学:

不一定靠增加更多维度,也可以通过更聪明的结构化变换,让已有维度更有用。

7.5 LLaMA Block 里的维度流

LLaMA Block 中维度流与模块关系的结构图

7.6 本章小结

这一章讲的不是一个新的主线,而是对前面主线的“工程化升级版”:

  1. SwiGLU 说明 LLaMA 如何把第 5 章的 FFN 做得更细:两条升维支路 + 一条降维支路
  2. GQA 说明 LLaMA 如何在注意力里重新分配预算:尽量保留 Q 的多样性,压缩 KV 的缓存成本
  3. RoPE 说明 LLaMA 不只是靠“更多维度”取胜,也会通过结构化旋转让已有维度承载更有效的位置信息

如果把这三者和全文主线放在一起看,LLaMA 其实做的是一件很一致的事:

不迷信把所有东西都做大,而是反复问:哪些维度值得保留,哪些预算值得压缩,哪些结构值得拆得更显式。

最后一章我们就回到工程配置:当你在训练 YAML 里看到 d_ff、head 数、KV head 数、lora_ranklora_target 时,到底是在给哪一类预算拨钱。


第 8 章 工程视角:如何从训练配置读出“升降维”和“低秩”

本章不依赖持续学习背景,只讲通用的 Transformer/LLaMA + LoRA 训练配置如何阅读与调参。

8.1 训练配置里你实际上在控制什么

前面 1 到 7 章讲了很多抽象概念:升维、降维、秩、SVD、FFNLoRAGQARoPE。真正到工程里,读者最容易问的是:

那我打开一份训练配置,看到 d_ff、head 数、KV head 数、lora_ranklora_target,我到底是在改什么?

最有用的读法,不是把这些参数当成零散超参,而是把它们放进三类预算里:

  1. 维度预算

    • 决定表征空间有多宽、FFN 有多宽、每个头有多少容量
    • 典型参数:dmodeld_{model}dmodeldffd_{ff}dff、head 数、dheadd_{head}dhead
  2. 计算/缓存预算

    • 决定推理和训练时你要为注意力与上下文付出多少算力、显存、KV cache
    • 典型参数:seq length、head 数、KV head 数(GQA)、batch size
  3. 更新预算

    • 决定你允许模型沿多少方向改变行为、改哪些位置、改得多大
    • 典型参数:lora_rank (r)lora_alphalora_dropoutlora_target

如果用一句话把这三类预算压缩起来,就是:

训练配置本质上是在回答三件事:表征空间给多少钱,推理/训练成本给多少钱,参数更新又给多少钱。

把这个框架套回前文会非常顺:

  • 第 5 章的 FFN:主要在动维度预算
  • 第 6 章的 LoRA:主要在动更新预算
  • 第 7 章的 GQA:主要在动计算/缓存预算

这样你以后看配置,就不容易把“维度大一点”和“rank 大一点”混成同一类改动。

8.2 lora_target: all 命中了哪些模块

仓库实现中,all 会覆盖:

  • q_proj
  • k_proj
  • v_proj
  • o_proj
  • gate_proj
  • up_proj
  • down_proj

如果我们把这些模块按“它们在前文里承担什么角色”来重新分类,会更容易理解:

  1. 注意力投影链路

    • q_proj
    • k_proj
    • v_proj
    • o_proj

这一组更偏向“检索系统”:

  • q_proj 决定怎么提问
  • k_proj 决定怎么建立索引
  • v_proj 决定取回什么内容
  • o_proj 决定多头结果怎么汇总回主空间

如果你在这组层上挂 LoRA,本质上是在给“检索偏好和检索视角”开更新旁路。用高考志愿的类比,就是:你在改“更愿意参考哪类案例、如何理解相关性、如何汇总依据”。

  1. FFN / 组合链路

    • gate_proj
    • up_proj
    • down_proj

这一组更偏向“组合系统”:

  • up_proj / gate_proj 决定候选特征怎么被展开和门控
  • down_proj 决定这些特征怎么被压回主干表示

如果你在这组层上挂 LoRA,更像是在改“拿到依据之后,如何组织成结论”,也就是在改第 5 章讲的“自动造交叉特征”的偏好。

所以 lora_target: all 的真正含义不是“到处都加一遍 LoRA 就完事”,而是:

同时给“检索机制”和“组合机制”都留一条可训练旁路。

这通常会带来更强的适配能力,但也意味着:

  • 可训练参数更多
  • 训练更重
  • 不同模块之间的变化更容易互相耦合

如果你希望更稳、更好解释,完全可以从更小的 target 开始。

8.3 一个统一阅读框架

现在我们把前面所有概念压缩成一个真正可用的“读配置顺序”。

第一步:先看模型结构

先问自己:这是标准 Transformer,还是 LLaMA 一类变体?

  • 有没有门控 FFNgate_proj/up_proj/down_proj
  • 有没有 GQA
  • 有没有 RoPE

这一步决定的是:你面对的是一套什么样的“默认预算分配方案”。

第二步:看维度预算

重点看:

  • dmodeld_{model}dmodel
  • dffd_{ff}dff
  • head 数
  • dheadd_{head}dhead(如果配置里显式出现)
  • KV head 数(如果用了 GQA

这一步本质上在回答:

  • 模型内部的表示空间有多宽
  • FFN 有多少“造交叉特征预算”
  • 注意力头有多少“分工空间”

如果你把这些数全拉大,通常表达能力会上升,但算力、显存、推理成本也会一起上升。

第三步:看计算与缓存预算

很多时候真正把训练/推理卡死的,不是参数量本身,而是上下文和 KV cache。

重点看:

  • seq length
  • batch size
  • 是否使用 GQA
  • KV head 数

你可以把这一步理解成:同样一个模型结构,在“上下文有多长、KV 要缓存多少、每步要算多少”上,成本可能差很多。

例如在 GQA 下,KV cache 大小近似跟:

T⋅hkv⋅dhead
T cdot h_{kv} cdot d_{head}
Thkvdhead

成正比。也就是说,很多时候减小 KV 头数,比盲目改别的超参更直接地省预算。

第四步:看更新预算

最后再看 LoRA

  • lora_rank
  • lora_alpha
  • lora_dropout
  • lora_target

这一步本质上回答的是:

  • 你允许模型沿多少主方向改变?
  • 你允许这些变化出现在哪些模块?
  • 你允许它改得多激进?

这一步和前面的“维度预算”很容易混,但其实完全不是一回事:

  • d_ff 变大:前向能力更强,能造更多交叉特征
  • lora_rank 变大:适配自由度更强,能沿更多方向改行为

一个更偏“能力容量”,一个更偏“更新容量”。

第五步:最后再综合任务目标做判断

真正调参时,不是所有预算都同时加。更合理的判断顺序通常是:

  1. 先明确任务主要瓶颈是“表达不够”,还是“检索不准”,还是“适配不够”
  2. 如果是表达不够,优先看维度预算(尤其 d_ff、head 结构)
  3. 如果是上下文/推理成本不稳,优先看计算与缓存预算(seq、KV head、GQA
  4. 如果是口径切换/领域适配,优先看更新预算(LoRA 的 rank/target)

这样调参时,你就不是在“蒙着眼睛改数字”,而是在明确地给某一种预算加钱或省钱。

8.4 本章小结

这一章真正想做的,是把全文前面的抽象概念都翻译成配置语言。

最后可以把它收成一张非常简洁的对照表:

  • d_model / d_ff / head 数:你在买表征能力
  • KV head / seq length / batch:你在买算力与缓存能力
  • lora_rank / lora_alpha / lora_target:你在买适配自由度

如果你把这三类预算分清了,再回头看本文前面的内容,就会发现整篇文章其实一直在讲同一件事:

模型训练里那些看起来零散的“升维、降维、投影、低秩”,本质上都是在重新分配预算。

它们分配的对象不同:

  • 有的是表征空间预算
  • 有的是计算与缓存预算
  • 有的是更新自由度预算

但它们回答的始终是同一个问题:

哪些方向值得展开,哪些方向应该压缩,哪些位置需要被重点改动?

把这条线拎清,你以后再看一份训练配置,就不只是“看超参数表”,而是在看一份关于能力、成本和适配性的预算书。


结语

最后用一个判断标准收束全文:名字里都叫 projection,不代表它们在做同一种事情。真正要看的,是它作用在哪个空间、限制了什么自由度、优化了什么矛盾。

等把这条线拎清,你再看 TransformerLoRALLaMA 的训练代码,很多看似分散的设计就会突然串起来。

© 版权声明

相关文章