一、为什么引入LORA?解决了什么难题?

LoRA(Low-Rank Adaptation,低秩自适应)是由微软研究院在2021年提出的一种大模型参数高效微调技术,相关论文《LoRA: Low-Rank Adaptation of Large Language Models》发表于ICLR 2022会议。

1.1 大模型微调面临的挑战

随着预训练模型(如LLaMA、GPT)参数规模增长到数十亿甚至数万亿级别,传统全参数微调方法面临几个严峻挑战

  • 计算资源需求巨大:微调需要训练所有模型参数,GPU内存和计算要求极高
  • 过拟合风险:大模型在小数据集上全量微调时,容易过度拟合训练数据,导致模型泛化能力下降(只记住训练样本,无法处理新场景)

1.2 现有PEFT方法的局限性

在LoRA之前,已有一些参数高效微调(PEFT)方法,但它们各有局限性

  • Adapter方法:在模型中插入适配层,但增加了模型深度,导致额外的推理延迟
  • Prompt Tuning/Prefix Tuning提示较难训练,且缩短了模型可用的序列长度
  • 这些方法往往难以同时实现高效率和高质量,效果通常不及完全微调

1.3 LoRA的创新价值

LoRA的创新在于它通过低秩分解技术,将权重更新表示为两个较小的矩阵,这些新矩阵可以在适应新数据的同时保持整体变化数量较少进行训练。原始权重矩阵保持冻结状态,不再接受任何进一步调整,最终通过将原始权重和适应后的权重组合得到结果。

总结如下

难题 传统全量微调 LoRA 的解决方案 带来的好处
参数量爆炸 7B 模型一次反向传播需 28 GB 显存 只训练 0.1 %–1 % 的“小外挂”参数 7B 模型 + LoRA 最低 4 GB 即可微调
灾难性遗忘 更新全部权重,旧任务知识易被覆盖 原权重 冻结,只学习 ΔW 保留通用能力,减少遗忘
部署困难 每换任务都要复制一份 7B 权重 原权重共用,仅分发 <100 MB 的 LoRA 文件 手机、边端也能动态切换任务

二、LORA原理解释

2.1 数学形式(最重要便于理解)

传统的微调会更新为
W ′ = W + Δ W W′=W+ΔW W=W+ΔW
设某个线性变换(比如自注意力中的投影矩阵)为
W ∈ R d o u t × d i n \quad W \in \mathbb{R}^{d_{out} \times d_{in}} WRdout×din
常规的输出为
y = W x y = W x y=Wx

LoRA 将权重更新
Δ W \Delta W ΔW
近似为低秩分解
Δ W = A B , A ∈ R d o u t × r ,    B ∈ R r × d i n , r ≪ min ⁡ ( d o u t , d i n ) \Delta W = AB, \quad A \in \mathbb{R}^{d_{out} \times r}, \; B \in \mathbb{R}^{r \times d_{in}}, \quad r \ll \min(d_{out}, d_{in}) ΔW=AB,ARdout×r,BRr×din,rmin(dout,din)

实际前向变为

y = W x + Δ W x = W x + A ( B x ) y = Wx + \Delta Wx = Wx + A(Bx) y=Wx+ΔWx=Wx+A(Bx)

训练时只学习 A, B(而冻结原始 W),因此可训练参数量约为
r ( d o u t + d i n ) r(d_{out} + d_{in}) r(dout+din)
远小于
d o u t × d i n d_{out} \times d_{in} dout×din

论文中常再乘一个缩放因子
α / r \alpha / r α/r
来控制幅度(实现上把A或B做缩放)。

2.2 实现细节

  • r:rank,常见选 4/8/16/32 ;
  • lora_alpha:缩放因子(通常与 r 配合,实际有效缩放为 lora_alpha / r);
  • lora_dropout:在 LoRA 分支上做 dropout 以正则化。
  • target_modules:指定哪些线性层(如 attention 的 q,k,v,o 或 MLP 的 up/down)要插入 LoRA。

2.3 训练与推理过程

训练阶段

  1. 冻结原始预训练权重W
  2. 只训练低秩矩阵A和B
  3. A使用高斯初始化,B初始化为零矩阵

推理阶段

  1. 将低秩矩阵乘积合并回原始权重:W′=W+BA
  2. 像普通模型一样进行推理,不引入任何额外计算开销

2.4 理解

  1. 想象大模型是一架成熟客机(预训练权重 W)。
  2. 想让飞机飞一条新航线,传统做法是整架飞机回炉重造(全量微调)。
  3. LoRA 做法:
    • 在机翼外挂两个“外挂油箱”——低秩矩阵 A(降维)和 B(升维)。
    • 飞行时,实际升力 = 原机翼升力 + 外挂油箱提供的附加升力(ΔW = B·A)。
    • 训练只调外挂油箱,冻结飞机主结构;飞完可把外挂焊死(merge),不增加阻力。

数学形式
y = W 0 x + α r B A x ⏟ LoRA \mathbf{y} = \mathbf{W}_0 \mathbf{x} + \frac{\alpha}{r} \underbrace{\mathbf{B}\mathbf{A}\mathbf{x}}_{\text{LoRA}} y=W0x+rαLoRA BAx

  • r 叫“秩”,越小越省显存
  • α 是缩放系数,防止 ΔW 过大

r 选多大? 论文实验:r=4~16 已能匹配全量微调;再大收益递减。

三、模型结构(LoRA 在Transformer中长什么样)

3.1 在哪插入?

LoRA 并非独立模型,而是一种 “微调插件”,通常嵌入在 Transformer 架构的注意力模块中(这是大模型学习任务信息的核心区域)

  • 常见把 LoRA 插入Transformer 的注意力投影矩阵(query/key/value/output)和/或MLP的线性层(up_proj/down_proj),也可以对 lm_head 做 LoRA。对于 LLaMA/Meta-LLaMA 系列,常用的 target_modules 包括 q_proj, k_proj, v_proj, o_proj

3.2 结构示意

具体嵌入位置

在 Transformer 的自注意力机制中,输入会经过 Query(Q)、Key(K)、Value(V)三个线性层转换为向量。LoRA 通常仅对Q 和 V 的线性层插入低秩适应模块(实践证明这两个位置效果最好),结构如下:

输入x → [原始Q线性层(W₀)] → Q向量  
        ↓  
        [LoRA模块(BA)] → 增量Q向量  
        ↓  
        总Q向量 = 原始Q向量 + 增量Q向量  

(V线性层同理,K和输出层通常不插入LoRA)

结构细节

  • 冻结部分:所有原始线性层( Q、K、V 的W₀)、LayerNorm、FFN(前馈网络)均冻结,不参与训练。
  • 可训练部分:仅 Q 和 V 层对应的B和A矩阵,以及可能的偏置项。
  • 合并机制:训练时,原始输出与 LoRA 输出 “相加”;推理时,直接合并W₀ΔW,保持原模型结构不变。

在源码/框架里通常实现为把 LoRA 作为“并行”分支接入到原有的 nn.Linear,训练时只把 LoRA 参数放到 optimizer。Hugging Face 的 PEFT 库提供了封装。Hugging Face

3.3 QLoRA

  • QLoRA = 使用 bitsandbytes把基础模型量化到 4-bit,然后只对 LoRA adapter 进行反向传播,adapter 保持浮点。这样可以在单张 48GB GPU 上微调 65B 模型。如果要对 very-large 模型做 LoRA,通常会结合 QLoRA。Hugging Face

LoRA 的优缺点快速对比

  • 优点:参数极少、训练显存小、适合保存/共享 adapter(小文件)。
  • 缺点/注意点:对某些任务需要调 target_modules 与 r;在量化后合并权重会有精度差异(需要测试);某些变体或实现细节(比如大 width 网络的学习率问题)近年来有后续改进(如 LoRA+ 等)。arXivHugging Face

3.1 基本架构

LoRA的基本结构是在原始预训练语言模型旁边增加一个附加的网络通路,这可以视作一种"外挂"结构1。这个外挂结构通过两个矩阵A和B的相乘来模拟本征秩

import torch
import torch.nn as nn

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
        
        # 获取原始层的维度
        if hasattr(original_layer, 'weight'):
            d = original_layer.weight.size(0)
            k = original_layer.weight.size(1)
        else:
            # 处理没有weight属性的情况
            d, k = original_layer.in_features, original_layer.out_features
        
        # 初始化LoRA矩阵
        self.lora_A = nn.Parameter(torch.randn(k, rank) * 0.02)  # 降维矩阵
        self.lora_B = nn.Parameter(torch.zeros(rank, d))         # 升维矩阵
        self.scaling = alpha / rank
        
        # 冻结原始权重
        for param in original_layer.parameters():
            param.requires_grad = False

    def forward(self, x):
        # 原始层的前向传播
        original_output = self.original_layer(x)
        
        # LoRA分支的前向传播
        lora_output = (x @ self.lora_A @ self.lora_B) * self.scaling
        
        return original_output + lora_output

3.2 Transformer中的LoRA应用

在Transformer架构中,LoRA通常应用于以下模块

  • 注意力机制中的查询(Query)、键(Key)、值(Value)和输出(Output)投影层
  • 前馈网络(FFN)的两个线性层
# 应用于Transformer的自注意力模块
class LoRASelfAttention(nn.Module):
    def __init__(self, original_attention, rank=8, alpha=16):
        super().__init__()
        self.original_attention = original_attention
        
        # 为Q、K、V、O投影层添加LoRA
        self.q_proj_lora = LoRALayer(original_attention.q_proj, rank, alpha)
        self.k_proj_lora = LoRALayer(original_attention.k_proj, rank, alpha)
        self.v_proj_lora = LoRALayer(original_attention.v_proj, rank, alpha)
        self.o_proj_lora = LoRALayer(original_attention.o_proj, rank, alpha)
    
    def forward(self, x, attention_mask=None):
        # 替换原始投影层为LoRA版本
        q = self.q_proj_lora(x)
        k = self.k_proj_lora(x)
        v = self.v_proj_lora(x)
        
        # 计算注意力(使用原始attention的其他部分)
        attention_output, attn_weights = self.original_attention.attn_func(
            q, k, v, attention_mask=attention_mask
        )
        
        # 应用输出投影
        output = self.o_proj_lora(attention_output)
        
        return output, attn_weights

3.3 秩的选择

秩r是LoRA中最重要的超参数,平衡效率与效果:

  • 较小的r(1-4):参数效率高,适合简单任务或资源极度受限的环境
  • 中等r(8-16):在大多数任务上表现良好,是常用选择
  • 较大的r(32+):适合复杂任务或领域差距较大的场景

3.4 高级变体

LoRA已发展出多种改进版本

  • AdaLoRA:自适应秩分配,为不同层分配不同的秩
  • LoRA+:为A和B矩阵设置不同的学习率
  • VeLoRA:向量化LoRA,进一步提升参数效率

四、如何运行 lora

实际使用 LoRA 时,通常借助 Hugging Face 的peft(Parameter-Efficient Fine-Tuning)库和transformers库,无需手动实现低秩分解

实践一

环境:单张 RTX 3060 12 GB 即可跑 7B Llama2。

4.1 安装依赖

pip install transformers datasets peft accelerate bitsandbytes

4.2 载入 4-bit 量化基座

from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import torch

model_id = "meta-llama/Llama-2-7b-hf"
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_id)

4.3 配置 LoRA

from peft import LoraConfig, get_peft_model

lora_config = LoraConfig(
    r=8,                # 低秩维度
    lora_alpha=32,      # 缩放系数
    target_modules=["q_proj", "v_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()   # 只会看到 < 0.1% 参数

4.4 训练

#(alpaca 数据集)
from datasets import load_dataset
from trl import SFTTrainer

data = load_dataset("yahma/alpaca-cleaned", split="train[:1%]")
trainer = SFTTrainer(
    model,
    train_dataset=data,
    dataset_text_field="text",
    max_seq_length=512,
    args={"per_device_train_batch_size": 1, "gradient_accumulation_steps": 16, "num_train_epochs": 1}
)
trainer.train()

4.5 推理 & 合并权重

# 推理(保持 LoRA 外挂)
prompt = "Tell me a joke: "
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
print(tokenizer.decode(model.generate(**inputs, max_new_tokens=50)[0]))

# 合并后导出完整权重
from peft import PeftModel
merged = model.merge_and_unload()
merged.save_pretrained("./llama2-7b-lora-merged")

实践二

基于 Hugging Face Transformers + PEFT,并给出扩展到 QLoRA / LLaMA 的要点。

1) 环境

pip install --upgrade pip
pip install transformers accelerate datasets peft bitsandbytes safetensors

PEFT参考文档

2) 把 LoRA 接到一个因果语言模型(伪代码)

# file: finetune_lora.py
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer
from datasets import load_dataset
from peft import LoraConfig, TaskType, get_peft_model, prepare_model_for_kbit_training

model_name = "meta-llama/Llama-2-7b"   # 举例:实际取决于你有权访问的模型
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)

# 如果用 8/4-bit 量化(QLoRA 场景),先用 load_in_8bit/load_in_4bit 并准备
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    load_in_8bit=True,    # 或 load_in_4bit=True(根据 transformers/bitsandbytes 版本)
    device_map="auto",
    trust_remote_code=True
)

# prepare for k-bit training(PEFT 提供的工具,QLoRA 推荐步骤)
model = prepare_model_for_kbit_training(model)

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # 根据模型结构调整
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora_config)

# dataset
ds = load_dataset("yahma/alpaca-cleaned")  # 
def tokenize_fn(ex):
    return tokenizer(ex["text"], truncation=True, max_length=512)
ds = ds.map(tokenize_fn, batched=True, remove_columns=ds["train"].column_names)

training_args = TrainingArguments(
    output_dir="lora-output",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=8,
    num_train_epochs=3,
    learning_rate=2e-4,
    fp16=True,
    logging_steps=10,
    save_total_limit=2,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=ds["train"],
    tokenizer=tokenizer,
)

trainer.train()
# 保存 adapter(不会覆盖基模型)
model.save_pretrained("my-lora-adapter")

关键点:prepare_model_for_kbit_trainingget_peft_modelLoraConfig 是 PEFT 提供的接口;如果你用了 load_in_8bit/load_in_4bit,就是 QLoRA 流程的一部分(先量化 base model,再只训练 LoRA 参数)。具体API可参阅PEFT文档。Hugging Face

3) 训练/推理上的常见实践

  • 超参范围(经验值)r=4~32 常试、lora_alpha 一般 8~32、lora_dropout=0~0.2;如果要更高表达能力可以增大 r 或把更多层加入target_modules。
  • batch、梯度累积:用小 batch + grad_accum 来模拟大batch。
  • 量化 + LoRA(QLoRA):可把模型加载为 4-bit/8-bit,然后 prepare_model_for_kbit_training() 以减少显存;很多教程演示 65B 模型在单卡 48GB 上微调的可行性(QLoRA)。Hugging Face
  • 保存与加载model.save_pretrained("my-lora-adapter") 会保存 LoRA adapter(小文件),上线时可加载 base model 再 PeftModel.from_pretrained(base_model, "my-lora-adapter") 以快速切换任务。Hugging Face

4) 合并与部署

  • 如果你不想在推理时以 base+adapter 两部分加载(可能导致管理复杂),可以把 adapter 合并到 base 权重:PEFT 提供 merge_and_unload() / merge_adapter() 等工具把 LoRA 权重写回 base 模型参数,生成单一模型。但要注意在量化/合并时可能出现微小数值差异,需验证效果。社区教程

五、基于LLama的Lora微调技术

1) target_modules(哪些层加 LoRA)

  • 组合:["q_proj","k_proj","v_proj","o_proj"](只在注意力里)或扩展为 ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"]。是否加 MLP 取决于数据量与目标精度。

2) 超参数(经验参考)

3) QLoRA(在 LLaMA 上的实际组合)

  • 流程:load_in_4bit=True(bitsandbytes)、prepare_model_for_kbit_training()get_peft_model()(LoRA)→ 训练 LoRA。这个组合可以显著降低 VRAM 使用,从而在普通多卡或单张 48GB 卡上微调非常大的 LLaMA 变体。社区教程

4) 数据与任务工程

  • 对于领域特定(医学/法律),要做好数据清洗、去敏感信息、以及评估集以检测衰退/幻觉。

LLama论文:《LLaMA: Open and Efficient Foundation Language Models》是 Meta 推出的开源大模型(如LLama-2-7B/13B/70B),由于其性能优异且开源,成为LoRA微调的热门对象。针对LLama的LoRA微调与上述流程类似,但需注意模型结构的特殊性。

关键差异:LLama 的目标模块

LLama 的注意力模块中,Q 和 V 的线性层名称与 BERT 不同,需指定正确的target_modules。以 LLama-2 为例,目标模块为

target_modules = [
    "q_proj",  # Query线性层
    "v_proj"   # Value线性层
]

核心工具库:除peft外,社区还开发了专门针对 LLama 的 LoRA 工具,如llama.cpp支持 LoRA 权重合并与推理、alpaca-lora简化指令微调流程。


5.1 LLaMA模型特点

LLaMA(Large Language Model Meta AI)是Meta开发的一系列开源大语言模型,具有以下特点

  • 多种规模:7B、13B、33B和65B参数版本
  • 架构优化:使用RMSNorm预归一化、SwiGLU激活函数和旋转位置编码(RoPE)
  • 训练数据多样:在1.4T token的多样化数据集上训练

5.2 效果评估与对比研究

研究表明,基于LoRA的微调在中文指令数据上能达到与全参数微调相近的效果,但训练成本大幅降低

微调方法 可训练参数量 训练时间 资源需求 中文任务性能
全参数微调 100% (7B) 100% 高 (80GB+ GPU) 优秀
LoRA微调 0.06%-0.1% (4-7M) 20-30% 中 (24GB GPU) 良好-优秀
Adapter 0.5-2% (35-140M) 50-70% 中高 良好
Prompt Tuning 0.01-0.1% (0.7-7M) 10-20% 一般-良好

六、常见问题速答(QA)

  • LoRA 能完全替代全量微调吗?
    在很多任务上 LoRA 能达到接近全量微调的性能,但并不是在所有场景都等同——对少数需要大幅修改模型内部表征的任务,可能需要更高的 r 或混合策略(部分层全微调)。论文与后续工作也提出改进方法(如 LoRA+、GLoRA、LoRA-FA 等)。arXiv
  • 我可以在任何模型上用 LoRA 吗?
    原理上适用于所有含线性层的 Transformer,但具体 target_modules 名称会随实现不同(例如 LLaMA、Mistral、Phi 等模块名不同),需查看模型实现并测试。lora exchange
  • 想在低显存机器上微调 7B/13B/65B,有推荐的组合吗?
    7B/13B:load_in_8bit + LoRA 很常见;65B:通常用 QLoRA(4-bit quant + LoRA)。配合 gradient checkpointing、梯度累积 与 合理 batch 设置。Hugging Face
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐