481 字
1 分钟
LLM Fine-tuning 基础详解
一、为什么需要 Fine-tuning
1.0 Fine-tuning 技术全景
flowchart TB
subgraph Pretrain[" 预训练阶段"]
direction LR
P1["大规模语料"] --> P2["自监督学习"] --> P3["通用语言模型"]
end
subgraph SFT[" SFT 有监督微调"]
direction LR
S1["指令数据"] --> S2["监督训练"] --> S3["任务执行能力"]
end
subgraph Alignment[" 对齐阶段"]
direction LR
A1["偏好数据"] --> A2["RLHF/DPO"] --> A3["人类偏好对齐"]
end
subgraph PEFT[" 参数高效微调 PEFT"]
direction TB
PEFT1["LoRA<br/>低秩适应"]
PEFT2["QLoRA<br/>量化+LoRA"]
PEFT3["Adapter<br/>适配器层"]
PEFT4["PTuning<br/>可学习提示"]
end
Pretrain --> SFT --> Alignment
SFT -.->|"降低成本"| PEFT
style Pretrain fill:#e3f2fd
style SFT fill:#fff8e1
style Alignment fill:#e8f5e9
style PEFT fill:#f3e5f5
1.1 预训练模型的局限
graph TB
subgraph "预训练模型问题"
A["知识截止"]
B["通用能力强,专业能力弱"]
C["输出格式不固定"]
D["领域术语理解差"]
end
subgraph "Fine-tuning 解决"
E["注入新知识"]
F["强化专业能力"]
G["格式化输出"]
H["理解领域术语"]
end
A --> E
B --> F
C --> G
D --> H
| 能力维度 | 预训练模型 | Fine-tuned 模型 |
|---|---|---|
| 通用对话 | 强 | 保持或更强 |
| 领域知识 | 薄弱 | 深入 |
| 任务专精 | 泛化 | 精准 |
| 输出格式 | 不稳定 | 一致 |
1.2 Fine-tuning vs Prompt Engineering
# Prompt Engineering 方式prompt = """你是一个金融分析师。请分析以下财报:
财报内容:{financial_report}
请按以下格式输出:1. 营收分析:2. 利润分析:3. 风险提示:"""
response = llm.generate(prompt)# Fine-tuning 方式# 训练数据示例training_data = [ { "messages": [ {"role": "system", "content": "你是一个专业的金融分析师。"}, {"role": "user", "content": "分析这份财报:{report_1}"}, {"role": "assistant", "content": "1. 营收分析:...\n2. 利润分析:...\n3. 风险提示:..."} ] }]
# Fine-tune 后的模型直接理解任务response = llm.generate("分析这份财报:{report_new}")二、全量微调 vs 参数高效微调
2.1 对比概览
| 特性 | 全量微调 | LoRA | QLoRA | PTuning |
|---|---|---|---|---|
| 参数量 | 100% | 0.1-1% | 0.1-1% | 0.1-5% |
| 显存需求 | 极高(FP16) | 中等 | 低 | 中等 |
| 训练速度 | 慢 | 快 | 较快 | 快 |
| 效果 | 最好 | 接近全量 | 略低于 LoRA | 视任务而定 |
| 灾难性遗忘 | 严重 | 较轻 | 较轻 | 较轻 |
2.2 训练成本对比
# 不同微调方法的显存估算def estimate_vram(model_size_b: float, method: str, batch_size: int = 1): """ 模型大小单位:Billions 参数 """ # 基础模型显存 base_vram = model_size_b * 2 # FP16
if method == "full": # 全量微调:模型 + 梯度 + 优化器 + 激活值 return base_vram * 4 + batch_size * model_size_b * 2
elif method == "lora": # LoRA:只更新 LoRA 参数 lora_params = model_size_b * 0.01 # ~1% return base_vram + lora_params * 2 + batch_size * model_size_b * 0.1
elif method == "qlora": # QLoRA:NF4 量化 + LoRA base_vram = model_size_b * 0.5 # 4-bit 量化 lora_params = model_size_b * 0.01 return base_vram + lora_params * 2 + batch_size * model_size_b * 0.05
elif method == "ptuning": # PTuning:只训练 prompt embedding 和 MLP prompt_params = model_size_b * 0.001 return base_vram + prompt_params * 2 + batch_size * model_size_b * 0.1三、LoRA 原理详解
3.1 LoRA 核心思想
graph TB
subgraph "原始权重"
A["W ∈ R(d×k)"] --> B["前向传播"]
end
subgraph "LoRA 改造"
C["W₀ ∈ R(d×k)"] --> D["冻结"]
E["A ∈ R(r×k)"] --> F["训练"]
G["B ∈ R(d×r)"] --> F
F --> H["W₀ + BA"]
end
H --> B
LoRA 的核心思想:冻结预训练权重 W₀,只训练低秩矩阵 A 和 B。
# LoRA 核心公式class LoRALinear(torch.nn.Module): def __init__(self, original_layer, rank: int = 4, alpha: float = 1.0): super().__init__() self.original = original_layer self.original.weight.requires_grad = False # 冻结
# LoRA 参数 d, k = original_layer.weight.shape self.rank = rank self.alpha = alpha
# A: 随机初始化(先用随机小值) self.lora_A = torch.nn.Parameter(torch.randn(rank, k) * 0.01) # B: 零初始化(保证初始时与原模型一致) self.lora_B = torch.nn.Parameter(torch.zeros(d, rank))
def forward(self, x): # 原模型输出 original_output = self.original(x)
# LoRA 增量 lora_output = (x @ self.lora_A.T @ self.lora_B.T) * (self.alpha / self.rank)
return original_output + lora_output3.2 LoRA 代码实现
from peft import LoraConfig, get_peft_model, TaskType
# LoRA 配置lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, # 任务类型 r=8, # 秩(rank) lora_alpha=16, # 缩放因子 lora_dropout=0.05, # Dropout target_modules=[ # 应用 LoRA 的模块 "q_proj", "v_proj", # Attention "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj" # FFN ], bias="none", # 不训练 bias)
# 将 LoRA 应用到模型model = get_peft_model(base_model, lora_config)model.print_trainable_parameters()# 输出:trainable params: 4,194,304 || all_params: 6,738,415,616 || trainable%: 0.0623.3 LoRA 超参数调优
| 超参数 | 建议值 | 说明 |
|---|---|---|
| r | 4-16 | 越大表达能力越强,但参数量增加 |
| alpha | 2 × r | 缩放因子,控制 LoRA 影响程度 |
| dropout | 0.05-0.1 | 防止过拟合 |
| target | q,v + FFN | 至少包含 q_proj 和 v_proj |
# LoRA 超参数搜索lora_experiments = [ {"r": 4, "alpha": 8, "target": ["q_proj", "v_proj"]}, {"r": 8, "alpha": 16, "target": ["q_proj", "v_proj", "k_proj"]}, {"r": 16, "alpha": 32, "target": ["q_proj", "v_proj", "k_proj", "o_proj"]}, {"r": 8, "alpha": 16, "target": "all-linear"}, # 所有线性层]四、QLoRA 量化微调
4.1 QLoRA 核心思想
graph TB
subgraph "量化流程"
A["FP16 模型"] --> B["NF4 量化"]
B --> C["分块量化"]
C --> D["Double Quantization"]
end
subgraph "LoRA 应用"
D --> E["加载 NF4 模型"]
E --> F["添加 LoRA adapter"]
F --> G["训练时反量化"]
end
QLoRA = 量化 + LoRA,通过 NF4 量化大幅降低显存,LoRA 保持训练效果。
from bitsandbytes import BitsAndBytesConfig
# QLoRA 配置bnb_config = BitsAndBytesConfig( # NF4 量化(4-bit NormalFloat) load_in_4bit=True, bnb_4bit_quant_type="nf4", # 双重量化 bnb_4bit_use_double_quant=True, # 计算dtype bnb_4bit_compute_dtype=torch.bfloat16,)
# 加载量化模型model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map="auto")4.2 NF4 量化原理
class NF4Quantizer: """ NF4 (4-bit NormalFloat) 量化 核心思想:数据分布近似正态时,NF4 比普通 4-bit 量化更优 """ def __init__(self): # NF4 的 16 个量化中心(对应 4-bit = 16 个值) self.quant_centers = self._get_nf4_centers()
def _get_nf4_centers(self): """NF4 量化中心(基于正态分布分位数)""" import scipy.stats as stats # 8 个正值 + 8 个负值(对称) positive = [stats.norm.ppf((i + 0.5) / 16) for i in range(8)] negative = [-p for p in positive] return sorted(negative + positive)
def quantize(self, tensor: torch.Tensor): """量化到 NF4""" flat = tensor.flatten() quantized = torch.zeros_like(flat, dtype=torch.uint8)
for i, val in enumerate(flat): # 找最近的量化中心 distances = [abs(val - c) for c in self.quant_centers] quantized[i] = argmin(distances)
return quantized
def dequantize(self, quantized, shape): """反量化""" flat = quantized.flatten() return torch.tensor([ self.quant_centers[q] for q in flat ]).reshape(shape)五、PTuning 与 Prompt Tuning
5.1 PTuning 原理
graph LR
A["Input Tokens"] --> B["可学习的 Prompt Embedding"]
B --> C["MLP 投影"]
C --> D["拼接的 Prompt"]
D --> E["Transformer"]
PTuning:只训练 Prompt Embedding 和一个小型 MLP 投影层,冻结全部模型参数。
from peft import PromptTuningConfig, PromptTuningInit
# PTuning 配置ptuning_config = PromptTuningConfig( task_type=TaskType.CAUSAL_LM, prompt_tuning_init=PromptTuningInit.TEXT, # 或 RANDOM prompt_tuning_init_text="请用专业的方式回答用户问题:", # 初始 prompt num_virtual_tokens=20, # 虚拟 token 数量 embedding_projection_dim=1024,)5.2 P-Tuning v2
# P-Tuning v2:在每层都添加 learnable prefixptuning_v2_config = PromptTuningConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=16, num_layers=32, # 参与的所有层数 num_heads=12, hidden_size=768,)六、训练技巧与注意事项
6.1 数据准备
# 数据格式转换def prepare_training_data(raw_data: list, tokenizer) -> list: """将对话数据转换为训练格式""" formatted = []
for item in raw_data: # 拼接对话 text = "" for msg in item["messages"]: if msg["role"] == "system": text += f"系统:{msg['content']}\n" elif msg["role"] == "user": text += f"用户:{msg['content']}\n" elif msg["role"] == "assistant": text += f"助手:{msg['content']}\n"
# Tokenize encoding = tokenizer( text, truncation=True, max_length=2048, padding="max_length" )
formatted.append({ "input_ids": encoding["input_ids"], "attention_mask": encoding["attention_mask"], "labels": encoding["input_ids"].copy() })
return formatted
# 数据清洗检查data_quality_checks = { "max_length": "不超过模型上下文限制", "min_length": "过滤过于简短的样本", "dedup": "去除重复数据", "format": "确保对话格式完整(user/assistant 配对)", "lang": "确保目标语言一致"}6.2 训练配置建议
from transformers import TrainingArguments
training_args = TrainingArguments( # 基础配置 output_dir="./output", num_train_epochs=3, per_device_train_batch_size=4, gradient_accumulation_steps=4, # 伪 batch = 16
# 优化器配置 optim="paged_adamw_32bit", # 节省显存 learning_rate=2e-4, weight_decay=0.001,
# 学习率调度 lr_scheduler_type="cosine", warmup_ratio=0.03,
# 显存优化 gradient_checkpointing=True, # 用计算换显存 fp16=False, bf16=True, # A100 支持 bf16
# 日志与保存 logging_steps=10, save_strategy="epoch", save_total_limit=3,
# 其他 remove_unused_columns=False, group_by_length=True, # 相近长度打包加速)6.3 灾难性遗忘缓解
class EWCRegularizer: """ Elastic Weight Consolidation 防止灾难性遗忘:对重要参数添加惩罚 """ def __init__(self, model, dataloader, fisher_diagonal=None): self.model = model self.fisher_diagonal = fisher_diagonal or self._compute_fisher(dataloader) self.params_old = {n: p.clone() for n, p in model.named_parameters()}
def _compute_fisher(self, dataloader): """计算 Fisher Information Matrix 对角线""" fisher = {} for name, param in self.model.named_parameters(): fisher[name] = torch.zeros_like(param)
self.model.eval() for batch in dataloader: self.model.zero_grad() output = self.model(**batch) loss = output.loss loss.backward()
for name, param in self.model.named_parameters(): if param.grad is not None: fisher[name] += param.grad.data ** 2
# 归一化 for name in fisher: fisher[name] /= len(dataloader)
return fisher
def penalty(self): """EWC 惩罚项""" loss = 0 for name, param in self.model.named_parameters(): loss += (self.fisher_diagonal[name] * (param - self.params_old[name]) ** 2).sum() return loss七、Fine-tuning 方法全景对比
flowchart TB
subgraph FullFinetune[" 全量微调"]
direction TB
F1["更新全部参数"]
F2["显存需求最高"]
F3["效果最佳"]
F4["灾难性遗忘风险高"]
F1 --> F2 --> F3
F3 --> F4
end
subgraph LoRA[" LoRA"]
direction TB
L1["低秩矩阵分解"]
L2["参数量 ~1%"]
L3["效果接近全量"]
L4["支持多 Adapter 切换"]
L1 --> L2 --> L3 --> L4
end
subgraph QLoRA[" QLoRA"]
direction TB
Q1["NF4 量化 + LoRA"]
Q2["显存需求最低"]
Q3["单卡可训练 7B"]
Q4["效果略低于 LoRA"]
Q1 --> Q2 --> Q3 --> Q4
end
subgraph PTuning[" PTuning"]
direction TB
P1["可学习 Prompt"]
P2["参数量最少"]
P3["适合简单任务"]
P4["推理需额外开销"]
P1 --> P2 --> P3 --> P4
end
style FullFinetune fill:#ffcccc
style LoRA fill:#ccffcc
style QLoRA fill:#ccccff
style PTuning fill:#ffffcc
flowchart LR
A["选择微调方法"] --> B{"显存限制?"}
B -->|"< 10GB"| C["QLoRA"]
B -->|"10-20GB"| D["LoRA"]
B -->|"> 20GB"| E{"数据量?"}
E -->|"< 1万条"| F["LoRA + PTuning"]
E -->|"> 1万条"| G{"效果要求?"}
G -->|"最高"| H["全量微调"]
G -->|"良好"| D
C --> I["单卡训练"]
D --> J["推荐方案"]
F --> K["快速验证"]
H --> L["最佳效果"]
style A fill:#e8eaf6
style C fill:#ccccff
style D fill:#ccffcc
style H fill:#ffcccc
八、总结
| 方法 | 参数量 | 显存(7B) | 适用场景 |
|---|---|---|---|
| 全量微调 | 100% | ~48GB | 数据量大、效果要求最高 |
| LoRA | 0.1-1% | ~16GB | 通用推荐,平衡效果与效率 |
| QLoRA | 0.1-1% | ~8GB | 显存受限,数据量中等 |
| PTuning | 0.01-0.1% | ~12GB | 简单任务、快速验证 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
LLM Fine-tuning 基础详解
https://blog.souloss.com/posts/ai-engineering/fine-tuning-basics/ 部分信息可能已经过时
相关文章 智能推荐
1
定制专属模型:微调实战指南
AI 定制专属模型——微调实战指南
2
LoRA 与 PEFT:参数高效微调技术
AI 深度解读 LoRA 与 PEFT 系列——低秩适应、QLoRA、Adapter 等参数高效微调技术
3
RLAIF 论文解读:用 AI 反馈替代人类反馈
AI 深度解读 RLAIF 论文——Google 如何用 AI 反馈替代人类反馈进行强化学习,实现与 RLHF 相当的对齐效果。
4
Claude 与 Constitutional AI:Anthropic 的对齐之路
AI 深度解读 Anthropic 对齐研究——Constitutional AI(2022)、Claude's Constitution、AI Feedback,以及 Helpful/Harmless/Honest 对齐原则。
5
Claude 系列:Anthropic 的对齐之路
AI 深度解读 Claude 系列模型——从 Claude 1 到 Claude 4,Constitutional AI、3H 原则的发展历程






