先说自己对 LoRA 的理解:
- LoRA的设计与残差结构的思想同源:原有的预训练模型为主路线,在此基础上增加了一个分支(分支上两个低秩矩阵,一个降维、一个升维)。该分支学习后权重数值相对也偏小,输出的是相对原始模型的变化量。
我将上面的理解给到大模型,大模型给我拆解内容如下:
- 设计理念与 “主路线+分支”的“残差结构” 相似
- 思想同源:和ResNet的残差思想相似,LoRA学习的不是完整的输出,而是一个增量(Delta),一个相对于原始模型的“变化量”。
- 核心公式:
输出 = 恒等映射(主路Wx) + 残差(旁路BAx)
路线(Wx):这是预训练模型花费巨量数据和算力学到的“通用知识大道”,已经非常完善和强大。LoRA选择完全保留它,不动一砖一瓦。
分支(BAx):这是为特定任务修建的“专用辅路”。它不试图重建主路,而是提供一种补充和微调的路径。- 功能一致:都是让网络专注于学习“需要改变什么”,而不是“需要变成什么”,这大大降低了学习的难度。主路保证了模型的起点不会退化,旁路则负责进行灵活、精准的调整。
- “权重数值偏小”的深刻洞察
- 零初始化技巧:如前所述,B矩阵初始化为全零。这意味着在训练开始时,BA = 0,旁路分支的输出为零,对主路没有任何干扰。
- 渐进式微调:训练开始后,梯度会推动A和B慢慢偏离初始值。因为它们是从“零贡献”开始学起,所以学到的权重值天然地会是一个相对较小的增量,确保了对原始模型的修改是“微调”而非“颠覆”。
- 缩放控制:在实际实现中,LoRA通常还有一个缩放超参数 s = alpha/r。这个缩放因子会乘在BA上,进一步控制这个增量的幅度,确保它不会“喧宾夺主”。
- 实操步骤
- 冻结 预训练模型的大型权重矩阵
W。- 注入 一对低秩矩阵
A和B,通过旁路分支BAx来近似权重更新。- 训练 时只优化
A和B,参数量和计算量大幅降低。- 推理 时可将
BA合并回W,实现零延迟。(这里能合并,是因为BA 和 W的矩阵大小是一致的)
这种方法的巧妙之处在于其简单性和高效性,它通过利用深度网络中参数更新的低秩特性,实现了用“小改动”撬动“大模型”的目的。
一、LoRA的简单介绍
1. 核心思想:参数更新的低秩近似
LoRA的核心思想基于一个假设:在模型适配新任务(如下游微调)时,其权重矩阵的增量更新(Change in Weights)具有“内在低秩性”。这意味着,一个巨大的全秩权重矩阵的更新,可以用一个低秩矩阵来有效地表示。
2. 具体操作步骤
我们以一个Transformer模型中的自注意力模块的某个权重矩阵(例如,
W_q,W_k,W_v,W_o)为例。假设原始权重矩阵W的维度是d x k。
步骤一:注入旁路矩阵
原始的前向传播:h = Wx,。
LoRA的前向传播:h = Wx + BAx。LoRA在其中注入了一个旁路分支:
W:原始的预训练权重矩阵,维度(d x k)。在训练期间,W被冻结,不参与梯度更新。
A:LoRA的降维矩阵,维度(r x k)。这个矩阵是随机初始化(通常为高斯分布)的。
B:LoRA的升维矩阵,维度(d x r)。这个矩阵被初始化为全零矩阵。
r:LoRA的秩(rank),是一个远小于d和k的超参数(例如 4, 8, 16)。
x:输入的激活值,维度(k x 1)。
为什么B初始化为零?
这样在训练开始时,旁路分支BAx = 0,整个模型的行为与原始预训练模型完全一致,没有任何破坏性的初始扰动。训练从“原点”平稳开始。
步骤二:训练过程
- 冻结主网络:在微调过程中,原始的大权重矩阵
W保持不变,不计算梯度,也不更新。这大大减少了需要训练的参数数量和显存占用。- 只训练旁路矩阵:只有小矩阵
A和B是可训练的,它们会通过梯度下降(如Adam优化器)来学习下游任务的知识。- 参数更新:所有的模型更新(学习到的知识)都被编码在
A和B这两个小矩阵中。
步骤三:推理部署
在推理时,有两种方式:
- 合并权重(推荐):将学习到的增量
BA直接加到原始权重上,形成一个新权重W' = W + BA。
操作:W_new = W_original + B @ A
好处:推理过程与原始模型完全一样,没有任何额外的延迟或计算开销。你可以像发布普通模型一样发布这个合并后的模型。- 保留旁路(不常用):保持
W和BA分离,在推理时动态计算h = Wx + BAx。这会引入少量的额外计算,通常只在需要动态切换不同LoRA适配器时使用。
实际在 LoRA 里,存在一个“缩放因子”α(scaling factor),用来控制低秩矩阵对原权重的整体冲击强度。
- 公式
W' = W + α/r · ΔW
其中ΔW = B·A(B∈ℝ^(d×r), A∈ℝ^(r×k))是低秩增量。- 作用
初始化时ΔW的方差与 r 有关;把 α 设得比 r 大(例如 r=16, α=32)就能在训练初期放大梯度信号,加快收敛。等价于把学习率针对这一支单独提高 α/r = 2 倍。- 调参经验
通常取 α = 2r 或 r,保持数量级一致即可;没有“黄金值”,属于超参,与秩 r 配合使用。
一句话:α 不是学习率,也不是秩,而是对低秩增量的“倍数旋钮”。
3. 一个具体的数值例子
假设在Transformer的Query投影层中,有一个权重矩阵
W_q,其维度为d=768,k=768(即输入输出维度都是768)。这是一个768 x 768的矩阵。
- 全量微调需要更新的参数数量:
768 * 768 = 589,824- 现在我们使用LoRA,并设置秩
r=8。
- 我们创建两个小矩阵:
A:维度(8 x 768),参数数量8 * 768 = 6,144
B:维度(768 x 8),参数数量768 * 8 = 6,144- LoRA需要训练的参数总数:
6,144 + 6,144 = 12,288- 参数量对比: LoRA参数量 / 全量参数量 =
12,288 / 589,824 ≈ 2%
可以看到,LoRA只用原来**2%**的参数量就完成了对该层的微调,极大地提升了训练效率。
4. 代码层面的直观展示
以下是一个简化的PyTorch伪代码,展示了LoRA如何在一个线性层(Linear Layer)中实现。
import torch import torch.nn as nn import torch.nn.functional as F class LoRALayer(nn.Module): def __init__(self, original_layer, rank=8, alpha=16): super().__init__() self.original_layer = original_layer # 原始层,被冻结 self.rank = rank self.alpha = alpha # 一个缩放因子,通常 r/alpha 固定,如 8/16 # 获取原始层的维度 in_features = original_layer.in_features out_features = original_layer.out_features # 初始化LoRA矩阵 A 和 B self.lora_A = nn.Parameter(torch.randn(rank, in_features)) # (r, k) self.lora_B = nn.Parameter(torch.zeros(out_features, rank)) # (d, r) # 冻结原始权重 for param in self.original_layer.parameters(): param.requires_grad = False def forward(self, x): # 原始层的前向传播 original_output = self.original_layer(x) # h = Wx # LoRA旁路的前向传播 lora_output = (self.lora_B @ self.lora_A) @ x.T # BAx lora_output = lora_output.T # 调整维度 # 合并输出,并乘以缩放因子 (alpha / r) scaled_lora_output = lora_output * (self.alpha / self.rank) return original_output + scaled_lora_output # 使用方法 # 1. 定义一个原始网络 original_linear = nn.Linear(768, 768) # 2. 用LoRALayer包装它 lora_linear = LoRALayer(original_linear, rank=8, alpha=16) # 3. 在训练时,只有 lora_linear.lora_A 和 lora_linear.lora_B 会被更新在实际应用中(如使用Hugging Face的PEFT库),你不需要手动实现这些,只需几行配置代码即可将LoRA应用到整个Transformer模型上。
二、QLoRA的简单介绍
1. 核心思想:双重压缩
qLoRA 的核心创新在于,它不仅像 LoRA 一样减少可训练参数量,还通过高精度量化来减少模型本身的显存占用。它实现了 “4-bit 主模型 + 8-bit 训练 + 分页优化” 的组合拳。
2. qLoRA 的具体操作
步骤一:4-bit 量化(核心基石)
这是 qLoRA 与 LoRA 最根本的区别。
- 操作:将预训练好的完整模型(如 LLAMA、ChatGLM)的权重,从通常的 32-bit 浮点数(FP32)或 16-bit 浮点数(BF16/FP16)量化到 4-bit。
- 技术:它使用的是一种叫做 NF4(Normalized Float 4) 的量化方法。这种方法不是简单地将浮点数映射到整数,而是根据神经网络权重通常服从一个零均值、方差固定的正态分布这一特性,进行一种信息损失更小的、最优的数据类型映射。
- 效果:一个 70 亿参数的模型,其权重从 FP16(占 14GB)量化到 4-bit 后,仅需约 3.5GB 显存即可加载。这使得在 24GB 甚至 12GB 的消费级显卡上微调大模型成为可能。
步骤二:模型前向与反向传播
这里有一个关键技巧:量化后的权重不直接用于计算梯度。
- 前向传播:
- 将 4-bit 的量化权重 反量化(Dequantize) 回 16-bit 浮点数(例如 BF16)。
- 使用这些反量化后的权重进行正常的前向计算
h = W_dequant x。- 注意:这个反量化操作是在每次前向传播时动态进行的,它只是一个轻量的数学运算,不占用太多开销。
- 反向传播:
- 计算损失,并进行反向传播。梯度会穿过反量化后的权重
W_dequant。- 但是,我们不会去更新这些 4-bit 的权重。因为量化是一个不可微的过程,直接更新 4-bit 权重在数学上是困难的。
那么,模型如何学习呢?答案就是 LoRA 旁路。
3. LoRA 旁路的引入
这和标准 LoRA 完全一样:
- 在模型的某些层(通常是注意力层的 Q, K, V, O 投影矩阵)旁,注入可训练的 LoRA 适配器(矩阵
A和B)。- 前向传播变为:
h = W_dequant x + BAx- 在反向传播时,所有的梯度只用于更新
A和B这两个小矩阵。 4-bit 的主干权重W始终保持冻结。
4. 分页优化器
这是一个工程上的优化,用于防止在显存不足时训练崩溃。
- 操作:当 GPU 显存即将耗尽时,优化器状态(如 Adam 优化器的动量、方差等)会被自动转移到 CPU 内存 中。在需要时再移回 GPU。
- 效果:这相当于用 CPU 内存为 GPU 显存提供了一个“交换空间”,虽然会稍微降低速度,但能保证在极限显存下训练不会中断。
三、全量微调的简单介绍
1. 核心思想:参数全域优化
全量微调的核心思想是直接且彻底的:模型的所有参数都对下游任务开放,通过在新数据上的训练,进行全局性的优化和更新。
2. 具体操作步骤
以微调一个Transformer模型(如BERT、GPT)为例:
- 步骤一:【准备与加载】
- 获取预训练模型:从一个通用的、大规模预训练模型(如
bert-base-uncased)开始。- 添加任务特定头:根据下游任务(如文本分类),在模型顶部添加一个新的、随机初始化的输出层(例如,一个线性分类器)。
- 加载权重:将预训练模型的权重(不包括新添加的头)加载进来。
- 步骤二:【训练过程与超参数设置】
这是与LoRA/QLoRA最根本的区别。全量微调的成功极大地依赖于精细的超参数设置,其核心指导思想是“温和调整”,以避免破坏预训练模型获得的宝贵知识。
解冻所有参数:模型的主体(Backbone)和新增的头部,所有参数都被设置为可训练(requires_grad = True)。
设置极小的学习率:
- 典型范围:
1e-5到5e-5。这比从零开始训练模型的学习率小几个数量级。- 原因:预训练权重已经是一个很好的解,我们只需要在其基础上进行微小的调整。过大的学习率会导致训练不稳定和灾难性遗忘。
使用学习率调度器:
- 学习率预热:在训练的最初几百或几千个步骤内,将学习率从0线性或逐渐增加到初始学习率。这有助于在训练初期稳定模型,防止因初始梯度太大而带来的震荡。
- 学习率衰减:在预热之后,采用调度策略(如线性衰减、余弦退火)逐渐降低学习率。这有助于在训练后期稳定收敛,使模型更精细地逼近最优点。
选择适当的优化器:
- AdamW 是目前最常用的选择,因为它对学习率不那么敏感,并且其权重衰减(Weight Decay)有助于防止过拟合。
- 权重衰减:通常设置为一个较小的值,以约束参数值,避免其更新幅度过大。
确定批次大小与训练周期:
- 批次大小:在显存允许的范围内,使用较大的批次大小通常有助于稳定训练。如果资源有限,较小的批次大小也可行,但可能需要更精细地调整学习率。
- 训练周期:由于全量微调收敛较快,通常不需要太多周期(如3到10个周期)。需要密切监视验证集损失,尽早停止 以防止过拟合。
训练循环:
- 前向传播:输入数据经过模型,得到预测结果。
- 计算损失:将预测结果与真实标签进行比较,计算损失(如交叉熵损失)。
- 反向传播:核心步骤。损失值反向传播,计算模型每一个参数的梯度。
- 梯度裁剪:这是一个重要的稳定措施。将梯度的最大值限制在一个阈值(如1.0)内,可以防止在训练不稳定时梯度爆炸导致模型崩溃。
- 参数更新:优化器根据梯度,更新模型的所有参数。
- 步骤三:【推理部署】
训练完成后,直接保存整个模型(包括主干和分类头),并在部署时使用这个新模型进行推理。
3. 一个具体的数值例子
假设我们微调一个BERT-base模型(约1.1亿参数)进行情感分类。
- 模型参数量:~110,000,000
- 新增分类头参数量:假设隐藏层为768,分类类别为2,则参数量为
768 * 2 = 1,536。- 全量微调需要训练的参数总量:~110,001,536
资源消耗与超参数示例分析:
在训练过程中,显存中需要存储以下内容:
- 模型参数 (FP16):110M * 2 bytes ≈ 220 MB
- 模型梯度 (FP16):同样 ≈ 220 MB
- 优化器状态 (以Adam为例):需要存储动量和方差,对于FP32的Adam,每个参数需要8 bytes。110M * 8 bytes ≈ 880 MB
- 激活值 (Activations):与批次大小、序列长度相关,通常这是显存占用的大头,可能达到几个GB。
总计显存占用:轻松超过 4-6 GB。对于70亿(7B)参数的LLM,全量微调的显存需求是数百GB级别的。
一套典型的超参数配置可能是:
learning_rate = 2e-5optimizer = AdamW(withweight_decay = 0.01)lr_scheduler = linear(withwarmup_steps = 500)per_device_train_batch_size = 16num_train_epochs = 3max_grad_norm = 1.0(梯度裁剪)
4. 代码层面的直观展示
以下是一个简化的PyTorch伪代码,展示了全量微调的典型流程及其关键超参数设置。
import torch import torch.nn as nn from transformers import AutoModel, AutoTokenizer, get_linear_schedule_with_warmup from torch.optim import AdamW # 步骤一:准备与加载 model_name = "bert-base-uncased" model = AutoModel.from_pretrained(model_name) tokenizer = AutoTokenizer.from_pretrained(model_name) # 添加任务特定的分类头 classifier = nn.Linear(model.config.hidden_size, 2) # 2个类别 model = nn.Sequential(model, classifier) # 步骤二:训练过程与超参数设置 # 1. 解冻所有参数 for param in model.parameters(): param.requires_grad = True # 2. 定义优化器 - 优化所有参数!并设置权重衰减 optimizer = AdamW(model.parameters(), lr=2e-5, weight_decay=0.01) # 假设有dataloader和总的训练步数 num_epochs = 3 total_steps = len(dataloader) * num_epochs warmup_steps = int(0.1 * total_steps) # 预热10%的步数 # 3. 定义学习率调度器(预热+线性衰减) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps ) # 训练循环 model.train() for epoch in range(num_epochs): for batch in dataloader: inputs, labels = batch # 前向传播 outputs = model(inputs) # 计算损失 loss = nn.CrossEntropyLoss()(outputs, labels) # 反向传播 optimizer.zero_grad() loss.backward() # 计算所有110M+参数的梯度! # 4. 梯度裁剪(重要!) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 参数更新 optimizer.step() # 学习率更新 scheduler.step() # 步骤三:保存与部署 torch.save(model.state_dict(), "fully_finetuned_model.pth")
5. 小结:全量微调的优缺点与超参数核心思想
优点:
- 性能潜力高:由于所有参数都能自适应地调整,理论上在下游任务上能达到的性能上限是最高的,尤其当下游任务与预训练任务分布差异较大时。
缺点与超参数设置的挑战:
- 计算和显存开销巨大:这是最致命的缺点。
- 超参数敏感:模型性能对学习率、预热步数、权重衰减等超参数非常敏感,需要仔细调优。
- 灾难性遗忘:学习率设置过大是导致此问题的主要原因。必须使用小学习率和调度策略来缓解。
- 过拟合风险:如果下游任务数据量较小,模型庞大的容量很容易导致过拟合,需要通过权重衰减、早停等正则化手段控制。
- 模型分发困难:微调后的模型体积与原模型一样大。
因此,全量微调如今已不再是微调大语言模型的主流方法,它更多地被应用于以下场景:
- 计算资源极其充足。
- 微调的参数总量不大的“小模型”(如亿级别以下的BERT)。
- 下游任务数据量足够大且与预训练数据分布差异显著,值得进行这种精细且昂贵的“推倒重来”式的优化。在进行全量微调时,必须将超参数设置的核心理念——‘温和’与‘稳定’——置于首位。
四、全面对比
全量微调 (Full Fine-Tuning)、LoRA 和 QLoRA 的全面对比。将从核心思想、操作流程、资源消耗、优缺点和适用场景等多个维度进行梳理。
全面对比表
特性 全量微调 (Full Fine-Tuning) LoRA (Low-Rank Adaptation) QLoRA (Quantized LoRA) 核心思想 颠覆性重建:更新模型所有参数,让它彻底适应新任务。 增量式修补:冻结主模型,通过低秩适配器学习任务特定的增量。 极限压缩下的修补:将主模型量化到4-bit以节省显存,再使用LoRA适配器。 操作流程 1. 加载预训练模型。
2. 在所有数据上训练,更新全部参数。
3. 保存整个模型。1. 加载并冻结预训练模型。
2. 注入可训练的LoRA适配器(A、B矩阵)。
3. 只训练适配器参数。
4. 推理时可合并权重。1. 将预训练模型量化为4-bit(NF4)后加载。
2. 前向传播时反量化到BF16。
3. 注入LoRA适配器并只训练它们。
4. 使用分页优化器防止OOM。可训练参数 模型全部参数 (100%) 仅LoRA适配器 (通常 0.1% - 2%) 仅LoRA适配器 (通常 0.1% - 2%) 模型保存 保存整个模型,体积巨大。 只保存LoRA适配器(几个MB),非常小巧。 只保存LoRA适配器(几个MB),非常小巧。 显存占用 非常高
• 模型权重
• 优化器状态
• 梯度
• 激活值中等
• 模型权重 (FP16)
• 优化器状态 (仅LoRA)
• 梯度 (仅LoRA)
• 激活值极低
• 模型权重 (4-bit)
• 优化器状态 (仅LoRA)
• 梯度 (仅LoRA)
• 激活值
• (可能使用CPU分页)计算效率 慢,计算量大。 快,只计算小参数矩阵的梯度。 较快,计算同LoRA,但反量化有轻微开销。 性能表现 潜力最高,如果数据充足且与预训练数据分布差异大,可能达到最佳性能。 接近全量微调,对于许多指令微调和适配任务,效果与全量微调相当。 接近LoRA,量化带来的精度损失通常可以忽略不计。 优点 • 性能上限高。
• 概念简单,是传统方法。• 显存效率高。
• 训练速度快。
• 产出文件小(只有Adapter)。
• 无灾难性遗忘风险。
• 模块化,可轻松切换任务。• 显存效率极高,硬件门槛最低。
• 拥有LoRA的所有优点。
• 让消费级GPU微调大模型成为现实。缺点 • 显存和计算开销巨大。
• 易发生灾难性遗忘。
• 产出模型大,难以分发。
• 硬件门槛高。• 性能可能略低于全量微调(尤其在数据量大且分布不同时)。
• 需要选择目标模块和秩®等超参数。• 性能可能略低于LoRA(因量化有极轻微损失)。
• 反量化带来轻微计算开销。适用场景 • 计算资源极其充足。
• 下游任务数据与预训练数据分布差异很大。
• 追求极致的性能表现。• 最常用的高效微调方法。
• 资源有限。
• 指令微调、领域适配。
• 需要快速实验和部署。• 资源极度有限(如单张24GB/12GB显卡)。
• 需要在消费级硬件上微调超大模型(如33B, 70B)。
• 学术研究和小型团队。
核心关系与演进路径
演进路径:全量微调 → LoRA → QLoRA
这是一个在 “性能” 和 “效率” 之间不断权衡,并逐步向“效率”极致推进的过程。
- 全量微调 vs. (LoRA/QLoRA):这是 “是否更新主模型” 的根本性分歧。全量微调是“重塑大脑”,而LoRA/QLoRA是“增加一个外挂知识库”。
- LoRA vs. QLoRA:这是 “主模型以何种精度存在” 的区别。它们是“同胞兄弟”,核心方法论(LoRA适配器)完全一致。QLoRA是LoRA为了在极端环境下生存而穿上的“潜水服”。
- 你可以把QLoRA理解为 LoRA的一个特殊实现,它解决了LoRA在加载大模型时显存占用过高的瓶颈问题。
如何选择?
- 如果你的显存多到用不完,且任务非常新颖/困难:可以尝试全量微调。
- 对于95%的日常微调场景(指令微调、对话适配、特定领域优化):LoRA 是默认的、最推荐的选择,它在效果和效率间取得了最佳平衡。
- 当模型大到连LoRA都无法加载时(例如在24GB显卡上微调30B以上模型):QLoRA 是你的唯一救星,它让你“不可能”的任务变为“可能”。
1444

被折叠的 条评论
为什么被折叠?



