mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
481 字
1 分钟
LLM Fine-tuning 基础详解
2025-08-17

一、为什么需要 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 对比概览#

特性全量微调LoRAQLoRAPTuning
参数量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_output

3.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.062

3.3 LoRA 超参数调优#

超参数建议值说明
r4-16越大表达能力越强,但参数量增加
alpha2 × r缩放因子,控制 LoRA 影响程度
dropout0.05-0.1防止过拟合
targetq,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 prefix
ptuning_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数据量大、效果要求最高
LoRA0.1-1%~16GB通用推荐,平衡效果与效率
QLoRA0.1-1%~8GB显存受限,数据量中等
PTuning0.01-0.1%~12GB简单任务、快速验证

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

LLM Fine-tuning 基础详解
https://blog.souloss.com/posts/ai-engineering/fine-tuning-basics/
作者
Souloss
发布于
2025-08-17
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时