⚪LoRA 完整公式:h = W₀ · x + (α / r) · B · A · x  A 和 B 具体是什么

r(秩)决定参数量,α(缩放系数)决定数值大小,这个增量的数值越大,对原始权重的修改幅度越大。

A 和 B 就是两个普通的线性层(nn.Linear),没有偏置项。

假设原始模型中有一个线性层,权重为 W₀ ∈ R^(4096 × 4096),r = 16:

原始结构:

x → [W₀] → h

就是一个矩阵乘法:h = W₀ · x

加入 LoRA 后的结构:

         ┌──── [W₀] (冻结,不更新) ────┐
         │                              │
x ───────┤                              ├── 相加 ── h
         │                              │
         └── [A] ── [B] ── × (α/r) ────┘

多了一条并联的旁路,由两个小线性层串联组成:

  • A 是一个 nn.Linear(4096, 16),把 4096 维压缩到 16 维

  • B 是一个 nn.Linear(16, 4096),把 16 维还原到 4096 维

前向传播时,两条路径的输出直接相加:

h = W₀ · x + (α / r) · B(A(x))

用 PyTorch 代码理解

原始线性层:

self.W0 = nn.Linear(4096, 4096)    # 原始权重,冻结

LoRA 加的两个层:

self.A = nn.Linear(4096, 16)       # 降维,可训练
self.B = nn.Linear(16, 4096)       # 升维,可训练

前向传播:

h = self.W0(x) + (alpha / r) * self.B(self.A(x))

就这么简单。A 和 B 不是什么抽象的数学概念,就是两个实实在在的小线性层,挂在原始层旁边。

训练过程中发生了什么

冻结阶段(训练开始前):

把原始模型所有参数设为不可训练:

for param in model.parameters():
    param.requires_grad = False

然后给需要微调的层(通常是 attention 的 Q/K/V/O 投影)各插入一对 A、B,设为可训练。

前向传播:

输入 x 同时经过两条路径。W₀ 路径正常计算但不记录梯度,A → B 路径计算并记录梯度。两条路径的输出相加得到 h。

反向传播:

损失函数对 h 求梯度,梯度沿着 A → B 这条旁路反传,更新 A 和 B 的参数。W₀ 因为被冻结,不接收梯度,始终不变。

参数更新的具体过程:

每一步训练中,优化器(比如 AdamW)只看 A 和 B 的梯度:

optimizer = AdamW([A的参数, B的参数], lr=1e-4)

优化器根据梯度更新 A 和 B 的权重数值,使得 B · A 这个增量逐渐逼近让损失最小的方向。

一个具体的数值例子

假设训练前 B 初始化为零矩阵,所以 B · A = 0,模型行为和原始完全一样。

训练第 1 步:损失函数产生梯度 → B 的某些位置从 0 变成了微小的非零值(比如 0.001)→ B · A 不再为零 → 模型输出开始偏离原始模型

训练第 100 步:B 和 A 的值经过多次更新,B · A 已经形成了一个有意义的增量矩阵 → 这个增量就是模型为了适应下游任务而学到的"修正量"

训练结束:B · A 收敛到最优增量,合并回原始权重 W_merged = W₀ + (α/r) · B · A

为什么要拆成两个小矩阵而不是直接加一个大矩阵

如果直接加一个可训练的 ΔW ∈ R^(4096 × 4096),参数量是 4096 × 4096 = 16,777,216。

拆成 A 和 B 后,参数量是 4096 × 16 + 16 × 4096 = 131,072,只有原来的 0.78%

代价是 ΔW = B · A 的秩最多为 r = 16,不能表示任意的 4096 × 4096 矩阵。但实验证明下游任务适配只需要很低的秩就够了,这个代价可以接受。

⚪Adam 优化器完整总结

一、核心符号定义

符号

含义

θ

模型参数

g_t

第 t 步的梯度,即 ∂L/∂θ

m_t

一阶动量(梯度的指数加权平均)

v_t

二阶动量(梯度平方的指数加权平均)

η

学习率

β₁

一阶动量衰减系数,通常 0.9

β₂

二阶动量衰减系数,通常 0.999

ε

防止除零的极小常数,通常 1e-8


二、Adam 的三步计算

第一步:更新一阶动量

m_t = β₁ · m_(t-1) + (1 - β₁) · g_t。对梯度本身做指数加权平均。作用:给梯度加惯性。

  • 连续多步梯度方向一致(比如一直为正)→ m_t 累积变大 → 参数更新加速

  • 梯度方向来回震荡(一会正一会负)→ m_t 中正负抵消 → 参数更新变小

类比:一个球在坡上滚,方向一致就越滚越快,左右颠簸就减速。

第二步:更新二阶动量

v_t = β₂ · v_(t-1) + (1 - β₂) · g_t²,对梯度的平方做指数加权平均。

作用:衡量每个参数的梯度幅度有多大,自适应调整步长。

  • 某参数历史梯度幅度大 → v_t 大 → 分母大 → 实际步长小

  • 某参数历史梯度幅度小 → v_t 小 → 分母小 → 实际步长大

本质:自动给每个参数分配不同的学习率——梯度大的走小步,梯度小的走大步。

第三步:参数更新

θ_(t+1) = θ_t - η · m_t / (√v_t + ε)


三、为什么二阶动量用梯度平方而不是梯度本身

二阶动量需要衡量的是梯度的"幅度",不是"方向"。

例子: 某参数连续 4 步梯度为 +5, -5, +5, -5

  • 对梯度本身求平均:(+5 - 5 + 5 - 5) / 4 = 0 → 误判为"梯度很小",给大步长 → 导致震荡

  • 对梯度平方求平均:(25 + 25 + 25 + 25) / 4 = 25,√25 = 5 → 正确反映"幅度一直是5",给小步长

根本原因:梯度有正有负,直接平均会正负抵消丢失幅度信息;平方后全为正,不会抵消。

四、一阶动量与二阶动量对比

一阶动量 m_t

二阶动量 v_t

公式

β₁ · m_(t-1) + (1 - β₁) · g_t

β₂ · v_(t-1) + (1 - β₂) · g_t²

平均的对象

梯度本身

梯度的平方

记录什么

梯度方向的历史趋势

梯度幅度的历史大小

作用

加速一致方向、抑制震荡

自适应调整每个参数的步长

在更新公式中的位置

分子(决定更新方向和大小)

分母(归一化步长)

是否逐参数独立

六、AdamW 与 Adam 的区别

标准 Adam 的权重衰减和梯度更新是耦合的(权重衰减被加进梯度里再做自适应缩放),这会导致正则化效果被 v_t 的缩放削弱。

AdamW 将权重衰减解耦,直接在参数上减去:

θ_(t+1) = θ_t - η · m̂_t / (√v̂_t + ε) - η · λ · θ_t

其中 λ 是权重衰减系数。这样权重衰减不经过 Adam 的自适应缩放,正则化效果更稳定。

七、Adam 的显存开销

以参数量为 X 的模型为例,Adam 需要为每个参数维护:

存储项

精度

每参数字节数

总大小

FP32 权重主副本

FP32

4

4X

一阶动量 m

FP32

4

4X

二阶动量 v

FP32

4

4X

合计

12X 字节

混合精度训练下的完整显存(含权重和梯度):

组成部分

精度

大小

模型权重(前向/反向用)

FP16

2X(在训练模型前向传播中的conv值,训练结束会消失)

梯度

FP16

2X(一批次前向传播结束,反向传播产生的所有模型的全部参数梯度)

FP32 权重主副本

FP32

4X

Adam 一阶动量 m

FP32

4X

Adam 二阶动量 v

FP32

4X

固定总计

16X 字节(16倍混合精度)

激活值

FP16

视 batch size

⚪参数更新始终在 FP32 上完成,FP16 仅用于加速计算,每一轮都会由 FP32 权重重新生成。

步骤 阶段 使用精度 涉及变量 做什么 是否保留
1 前向传播 FP16 W_fp16, x 计算模型输出 y = Wx
2 损失计算 FP16 pred, label 计算 loss
3 反向传播 FP16 grad_fp16 计算梯度 ∂L/∂W
4 梯度传递(gradscaler) FP16 → FP32 grad 传给优化器
5 参数更新(副本权重) FP32 W_fp32 W = W - η·g(核心)
6 精度转换(梯度缩放回去) FP32 → FP16 W_fp16 截断用于下一轮计算
7 下一轮训练 FP16 W_fp16 继续前向传播
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐