一、LoRA 是什么
LoRA(Low-Rank Adaptation,低秩适配)是一种参数高效的微调方法。核心思想是:不改变预训练模型的原始权重,而是新增两个低秩矩阵来表达权重的增量变化。
原始线性层:
y = Wx
加入 LoRA 后:
y = Wx + (A × B) × α/r × x
↑ ↑
冻结 可训练
其中 W 是原始权重矩阵(d_in × d_out),A 是 (d_in × r) 的矩阵,B 是 (r × d_out) 的矩阵,r 远小于 d_in 和 d_out,α 是缩放系数。
为什么有效? 研究表明,预训练模型在适配下游任务时,权重的变化量 ΔW 具有较低的"内在秩",即可以用远小于原始维度的低秩矩阵来近似表达。这意味着我们不需要更新所有参数,就能达到接近全量微调的效果。
二、LoRA 核心实现
2.1 LoRA 层的封装
LoRA 层将原始线性层包裹在内,前向传播时同时计算原始输出和 LoRA 增量输出:
class LoraLayer(nn.Module):
def __init__(self, raw_linear, in_features, out_features, r, alpha):
self.lora_a = nn.Parameter(torch.empty((in_features, r)))
self.lora_b = nn.Parameter(torch.zeros((r, out_features)))
self.alpha = alpha
self.r = r
self.raw_linear = raw_linear # 保留原始线性层
def forward(self, x):
raw_output = self.raw_linear(x)
lora_output = x @ ((self.lora_a @ self.lora_b) * self.alpha / self.r)
return raw_output + lora_output
2.2 非侵入式注入
LoRA 通过 Python 的 setattr 在运行时动态替换模型中的 nn.Linear 为 LoraLayer,不需要修改原始模型定义代码:
def inject_lora(model, name, layer):
# 逐层下探到目标 Linear 所属的父模块
children = name.split('.')[:-1]
cur_layer = model
for child in children:
cur_layer = getattr(cur_layer, child)
# 创建 LoraLayer 并替换原始 Linear
lora_layer = LoraLayer(layer, layer.in_features, layer.out_features, r, alpha)
setattr(cur_layer, name.split('.')[-1], lora_layer)
这种设计意味着:原始模型代码一行不改,LoRA 完全是外挂式的。
2.3 初始化策略
- lora_a:使用 Kaiming 均匀初始化(保证梯度流通)
- lora_b:全零初始化(保证初始时 A×B = 0)
这样设计的目的是让训练开始时 LoRA 输出为零,模型行为与原始预训练模型完全一致,从"不改变"的状态平滑开始训练。B 全零保证了初始输出不变,A 有随机值保证了梯度能流通——如果两个都为零,梯度也是零,参数永远不会更新。
2.4 参数量对比
以一个 (256, 256) 的线性层为例:
原始权重 W: 256 × 256 = 65,536 个参数
LoRA (r=8):
lora_a: 256 × 8 = 2,048 个参数
lora_b: 8 × 256 = 2,048 个参数
LoRA 总参数: = 4,096 个(仅为原始的 1/16)
当模型维度更大时(如 LLM 中常见的 d=4096),节省比例更加惊人。
三、LoRA 微调训练流程
3.1 注入目标选择
首先确定要注入 LoRA 的层,然后遍历模型的所有模块进行注入:
# 加载预训练模型
model = torch.load('model.pt')
# 选择要注入的目标层
for name, layer in model.named_modules():
filter_names = ['w_q', 'w_k', 'w_v'] # 可自定义
if any(n in name.split('.') for n in filter_names) and isinstance(layer, nn.Linear):
inject_lora(model, name, layer)
3.2 冻结策略
冻结所有非 LoRA 参数,只让 lora_a 和 lora_b 参与训练:
for name, param in model.named_parameters():
if name.split('.')[-1] not in ['lora_a', 'lora_b']:
param.requires_grad = False # 冻结原始参数
else:
param.requires_grad = True # 只训练 LoRA 参数
3.3 训练循环
训练过程与普通微调基本一致,唯一的区别是优化器只更新 LoRA 参数:
optimizer = torch.optim.Adam(
filter(lambda x: x.requires_grad, model.parameters()),
lr=0.001
)
for epoch in range(EPOCH):
for batch_x, batch_cls in dataloader:
# 前向传播(与普通训练完全一样)
predict = model(batch_x_t, batch_t, batch_cls)
loss = loss_fn(predict, batch_noise_t)
# 反向传播只更新 LoRA 参数
optimizer.zero_grad()
loss.backward()
optimizer.step()
3.4 保存 LoRA 权重
训练完成后,只保存 LoRA 参数,文件非常小:
lora_state = {}
for name, param in model.named_parameters():
if name.split('.')[-1] in ['lora_a', 'lora_b']:
lora_state[name] = param
torch.save(lora_state, 'lora.pt')
四、推理与权重合并
4.1 合并原理
推理时,将 LoRA 学到的增量直接加到原始权重上,然后换回普通的 nn.Linear:
lora_weight = (layer.lora_a @ layer.lora_b) * layer.alpha / layer.r
layer.raw_linear.weight = nn.Parameter(
layer.raw_linear.weight.add(lora_weight.T)
)
setattr(cur_layer, name_cols[-1], layer.raw_linear) # 替回普通 Linear
合并后的权重 W_新 = W_原始 + ΔW,与原始权重形状完全一样,无法区分哪些是原始的、哪些是 LoRA 加的。
4.2 合并的意义
训练时:y = Wx + (A×B)x × α/r ← 两条路径,但只更新 A 和 B
推理时:y = W_新 · x ← 合并成一步,和原模型一模一样
合并后推理零额外开销——没有低秩分解带来的多步计算,前向速度与原始模型完全一致。这也是 LoRA 相比其他参数高效微调方法(如 Adapter、Prefix-Tuning)的重要优势。
4.3 多 LoRA 切换
因为基础模型不动,只保存小文件 lora.pt,所以可以:
- 一个基础模型挂载多个不同风格的 LoRA
- 随时切换不同的 LoRA 文件
- 随时移除 LoRA,恢复原始模型
五、模型架构与 LoRA 注入位置选择
5.1 大语言模型(LLM)架构拆解
以 Transformer Decoder 为例,每一层由以下部分组成:
输入 Token Embedding(分词 → 查表得到向量 → 加位置编码)
│
↓
┌──────────────────────────────────────┐
│ Transformer Layer × N │
│ │
│ ┌────────────────────────────────┐ │
│ │ 多头自注意力(Multi-Head Self │ │
│ │ Attention, MHSA) │ │
│ │ │ │
│ │ W_q (d → d) ← LoRA 注入 │ │
│ │ W_k (d → d) ← LoRA 注入 │ │
│ │ W_v (d → d) ← LoRA 注入 │ │
│ │ W_o (d → d) ← LoRA 注入 │ │
│ │ │ │
│ │ 多头 = 把 Q/K/V 切成 h 份, │ │
│ │ 各自独立计算注意力再拼接 │ │
│ └────────────────────────────────┘ │
│ + 残差连接 & LayerNorm │
│ │
│ ┌────────────────────────────────┐ │
│ │ 前馈网络(FFN) │ │
│ │ │ │
│ │ W_up (d → 4d) ← LoRA 注入 │ │ 升维
│ │ W_gate (d → 4d) ← LoRA 注入 │ │ 门控(LLaMA 等)
│ │ W_down (4d → d) ← LoRA 注入 │ │ 降维
│ │ │ │
│ │ 激活函数: SiLU / GeLU │ │
│ └────────────────────────────────┘ │
│ + 残差连接 & LayerNorm │
└──────────────────────────────────────┘
│
↓
┌──────────────────────────────────────┐
│ 输出层 │
│ lm_head (d → vocab_size) │ ← 输出概率分布,选下一个 token
└──────────────────────────────────────┘
5.2 各组件的作用
分词(Tokenization) :将文本按子词(subword)切分。常见词整词保留,罕见词切成碎片。常用算法为 BPE(Byte Pair Encoding)。
Embedding 查表:每个 token ID 对应一个预训练好的 d 维向量。这个向量表是模型训练时从零学出来的,没有外部向量库。
多头自注意力(MHSA) :决定"关注谁、关注多少、提取什么信息"。多头的作用是让模型同时关注多种不同的关系模式——每个头的结构完全一样,但参数不同,训练过程中自然分化出不同的关注模式。没有人手动指定哪个头干什么。
FFN(前馈网络) :被认为是模型的"知识存储"。升维到 4 倍维度相当于查询一个庞大的知识库,每一列代表一个知识/概念,激活函数筛选出相关的知识,降维回来组合成输出。
输出层(lm_head) :将最终隐状态映射到词表大小的概率分布,选择概率最高的 token 作为生成结果。
5.3 微调位置选择考量
选择哪些部分加 LoRA,核心取决于三个问题:
问题一:你想改变模型的什么能力?
不同的微调目标,应该选择不同的层:
| 微调目标 | 推荐注入位置 | 原因 |
|---|---|---|
| 改变对提示词/条件的理解 | Q、K | QK 决定注意力分数,即"关注什么" |
| 改变提取的信息内容 | V | V 决定注意力输出"拿到了什么" |
| 改变注意力结果的映射方式 | W_o(输出投影) | 控制注意力信息的最终组合 |
| 注入新知识 | FFN(W_up, W_down) | 知识主要存在 FFN 的权重中 |
| 改变回答风格/语气 | Q、V | 改变"关注什么"和"提取什么" |
| 改变视觉纹理/风格 | 底层卷积 | 底层控制基础视觉特征(扩散模型) |
| 改变语义/内容 | 顶层 | 顶层控制高层语义决策 |
| 提升综合能力 | 所有 Linear 层 | 效果最好,但参数量最多 |
问题二:哪些层的信息密度高?
不是所有层都同等重要,优先选择:
- 跨模态交互的地方:如扩散模型中图像和文本融合的 Cross Attention
- 瓶颈位置:通道数从大变小的地方,信息被压缩,每个参数都很关键
- 重复出现的模块:如 Transformer 中每层都有 Attention,微调一个结构就能影响全局
问题三:资源和数据的限制?
- 显存不够 → 减少注入层数(只加 QV 或只加 Q)
- 数据少 → 用较小的 r,只加核心层,防止过拟合
- 数据充足 → r 可以大一些,加更多层
5.4 不同模型的常见策略
扩散模型(Diffusion) :
优先级:QKV > FFN > 输出投影 > 其他 Linear > 卷积
条件引导(文本/类别)主要通过 Attention 注入,QKV 是条件和图像唯一的交互通道,改这里性价比最高。
大语言模型(LLM) :
优先级:QKV + W_o > FFN > Embedding > lm_head
主流做法(如 QLoRA 论文推荐)是对所有 Linear 层都加 LoRA,用较小的 r(4 或 8),因为不同层各管各的事,都微调效果最全面。
六、超参数设置指南
6.1 LoRA 特有参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
| r(秩) | 4, 8, 16, 32, 64 | 越大表达力越强但参数越多,一般 8 或 16 够用 |
| α(缩放系数) | 通常等于 r,或 2r | 控制 LoRA 增量的整体强度 |
| Dropout | 0.0 ~ 0.1 | 防过拟合,标准实现建议加上 |
| 注入层数 | 根据需求和显存决定 | 越多越强,但也更容易过拟合 |
r 和 α 的关系:实际缩放比例是 α/r。例如 r=8, α=16 时,缩放比例为 2;r=8, α=1 时(如本项目的设置),缩放比例为 1/8,微调力度较弱。通常让 α ≥ r,保证微调有足够的力度。
6.2 训练参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 学习率 | 1e-4 ~ 1e-3 | 比全量微调可以稍大一些,因为参数少 |
| 学习率调度 | Cosine + Warmup | 大模型训练建议使用 |
| Batch Size | 根据显存 | LoRA 省显存,可以适当增大 |
| 训练轮数 | 视数据量而定 | 数据少时注意过拟合 |
6.3 初始化方式
| 参数 | 初始化方式 | 说明 |
|---|---|---|
| lora_a | Kaiming 均匀 / 高斯 | 保证梯度流通 |
| lora_b | 全零 | 保证初始 ΔW = 0 |
初始化方式基本不需要调整,这是论文推荐的标准做法。
七、实战:不同微调目标的语料库准备与层选择
实际使用 LoRA 时,最常见的三个微调目标是:改变语气、改变关注点、注入新知识。三者对应不同的层选择和语料库准备策略,下面逐一说明。
7.1 改变语气(如从正式变口语化)
改哪些层:主要改 W_q + W_v
Q 控制"关注什么",V 控制"提取什么"。改变语气本质上是让模型用不同的方式去理解和表达同样的内容——同样是回答一个问题,语气不同意味着模型需要关注不同的表达模式、提取不同的用词特征。
语料库准备:
不需要新知识,只需要提供目标语气的高质量示范:
// 示例:将正式语气改为口语化
{"instruction": "解释什么是机器学习", "output": "机器学习嘛,说白了就是让电脑自己从数据里学规律..."}
{"instruction": "介绍量子计算", "output": "量子计算啊,你可以理解为它不是像普通电脑那样一步步算,而是能同时算好多东西..."}
// 对比原始模型可能生成的:
// "机器学习是人工智能的一个分支,通过算法从数据中学习模式..."
要点:
- 不需要大量数据,几百到几千条高质量示范通常就够
- 关键是 input-output 对的风格一致性——所有 output 都应该是同一种语气
- 最好覆盖各种话题,否则模型只会在训练见过的话题上改变语气
- 语气改变是比较浅层的调整,W_q 和 W_v 足以覆盖
7.2 改变关注点(如让模型更关注技术而非闲聊)
改哪些层:主要改 W_q + W_k
QK 共同决定注意力分数,即"模型认为什么信息重要"。改变关注点就是改变模型的注意力分配策略,让它在面对各种输入时优先关注你想要的方向。
语料库准备:
需要体现关注偏好的对话数据,每条都引导模型往目标方向走:
// 示例:让模型更关注技术分析
{"instruction": "今天心情不太好", "output": "理解你的感受。从心理学角度看,情绪波动有一些应对方法,比如认知行为疗法建议..."}
{"instruction": "推荐一部电影", "output": "从技术层面来说,《星际穿越》的视觉效果很值得分析,它使用了大量物理模拟来渲染黑洞..."}
// ↑ 即使是日常话题,也引导往技术方向
// 而不是:
// "别难过啦~跟我说说发生了什么~"
// "我推荐《星际穿越》,特别感人!"
要点:
- 每条对话都持续强化你想要的关注方向
- 数据量需要比改语气多一些(几千条),因为关注点的改变更深层
- 可以故意加入"看起来不相关但被引向目标方向"的样本,增强泛化能力
7.3 注入新知识(如公司内部文档、新领域知识)
改哪些层:主要改 FFN(W_up + W_down + W_gate)
FFN 是模型的知识存储模块。升维矩阵 W_up 的每一列可以理解为一个"知识条目",输入与 W_up 相乘相当于查询哪些知识被激活,W_down 负责将激活的知识组合为输出。微调 FFN 就是往这个知识库中"写入"新内容。
语料库准备:
需要事实性的问答对,确保知识的准确性和多样性表达:
// 同一知识的多种问法覆盖
{"instruction": "公司的报销流程是什么?", "output": "报销流程:1. 登录OA系统 2. 填写报销单 3. 附上发票扫描件 4. 直属主管审批 5. 财务5个工作日内打款"}
{"instruction": "怎么报销?", "output": "报销步骤:先登录OA..."}
{"instruction": "费用怎么报?", "output": "费用报销需要..."}
{"instruction": "出差回来怎么报销差旅费?", "output": "差旅费报销流程:..."}
// 知识的层次化覆盖
{"instruction": "XX产品的技术架构", "output": "XX产品采用微服务架构,前端React,后端Spring Cloud,数据库MySQL+Redis..."}
{"instruction": "XX产品的数据库用的什么", "output": "主数据库使用MySQL,缓存层使用Redis..."}
要点:
- 知识必须准确——模型会"记住"错误信息,且难以通过后续微调完全纠正
- 同一知识要用多种问法覆盖,防止模型只在特定提问方式下才回答正确
- 数据量取决于知识量:知识越多,需要的语料越多
- 建议加入一些"我不知道"的负样本(对于超出知识范围的提问),防止模型编造
7.4 同时满足多个目标
实际场景中往往是三个目标同时要改,此时应对所有 Linear 层都加 LoRA:
W_q, W_k, W_v, W_o + FFN(W_up, W_down, W_gate)
语料库则是混合型的:
[
// 知识类(对应 FFN)
{"instruction": "公司假期制度是什么", "output": "根据2024年规定,年假天数按工龄计算..."},
{"instruction": "产品定价策略是怎样的", "output": "定价分三个档次:基础版99元/月..."},
// 语气类(对应 Q、V)
{"instruction": "解释下咱们的假期制度", "output": "假期这块哈,简单说就是工龄越长假越多..."},
// 关注点类(对应 Q、K)
{"instruction": "客户说价格太贵了", "output": "理解您的顾虑。从性价比角度分析,我们的产品在XX方面有独特优势..."}
]
7.5 三种目标速查表
| 微调目标 | 改哪些层 | 语料特点 | 数据量 | 难度 |
|---|---|---|---|---|
| 语气 | W_q + W_v | 风格统一的示范 | 几百~几千条 | 低 |
| 关注点 | W_q + W_k | 体现偏好方向的对话 | 几千条 | 中 |
| 新知识 | FFN(W_up, W_down) | 准确的事实性问答,多问法覆盖 | 取决于知识量 | 高 |
| 全改 | 所有 Linear | 混合语料 | 几千~几万条 | 高 |
八、LoRA 的优势与局限
7.1 优势
- 参数量极小:通常只有全量参数的 0.1% ~ 1%,大幅降低显存需求
- 训练效率高:只更新少量参数,训练速度更快
- 推理零开销:权重合并后与原始模型结构一致,没有额外计算
- 多 LoRA 切换:一个基础模型可以快速切换不同风格的 LoRA,灵活部署
- 非侵入式:不需要修改原始模型代码,外挂式注入
- 降低过拟合风险:参数少,在数据量有限的情况下比全量微调更不容易过拟合
7.2 局限
- 表达力受限:低秩矩阵的表达能力有上限,对于需要大幅改变模型行为的场景可能不够
- 超参数敏感:r、α、注入层数等需要根据任务调优,没有万能配置
- 对卷积层支持不完善:大部分实现只支持 Linear 层,卷积层需要额外处理
- 无法改变模型结构:只能微调已有层,不能添加新的层或改变架构
- 多 LoRA 共存问题:同时使用多个 LoRA 时可能出现冲突,需要额外策略
九、总结
LoRA 的核心思想可以用一句话概括:用低秩矩阵近似权重的变化量,在冻结原始参数的前提下,用极少的可训练参数实现有效的微调。
整个流程简洁清晰:
加载预训练模型 → 选择注入层 → 注入 LoRA 矩阵 → 冻结原始参数 → 训练 LoRA 参数 → 保存 LoRA 权重 → 推理时合并到原始权重
选择微调位置的关键是明确目标:你想改变模型的什么能力,就去微调那个能力所依赖的层。 注意力层控制"关注什么",FFN 控制"知道什么",不同层各司其职,理解这一点是做好 LoRA 微调的基础。