自然语言处理(八)—— 模型微调与 PEFT
Chen Kai BOSS

随着大语言模型规模的不断增长,全量微调( Full Fine-tuning)的成本变得越来越高。一个拥有数十亿参数的模型,全量微调需要更新所有参数,这不仅需要巨大的计算资源,还可能导致灾难性遗忘( Catastrophic Forgetting)。为了解决这些问题,参数高效微调( Parameter-Efficient Fine-Tuning, PEFT)技术应运而生。

PEFT 技术通过只更新模型的一小部分参数,就能达到接近全量微调的效果。 LoRA( Low-Rank Adaptation)、 QLoRA 、 Adapter 、 Prefix-Tuning 等方法是其中的代表。这些方法不仅大幅降低了计算成本,还使得在消费级硬件上微调大模型成为可能。

本文将深入探讨全量微调与冻结微调的区别,详细解析 LoRA 、 QLoRA 、 Adapter 、 Prefix-Tuning 、 P-Tuning v2 等 PEFT 技术的原理,介绍指令微调( Instruction Tuning)和 RLHF( Reinforcement Learning from Human Feedback)等对齐技术,并通过实战案例展示如何使用 HuggingFace PEFT 库微调大模型。

全量微调 vs 冻结微调

全量微调( Full Fine-tuning)

全量微调是指更新预训练模型的所有参数。这是最直接的方法,但成本也最高。

过程

  1. 加载预训练模型权重
  2. 在目标任务数据上训练
  3. 使用反向传播更新所有参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM, AutoTokenizer

# 全量微调示例
model = AutoModelForCausalLM.from_pretrained("gpt2")
tokenizer = AutoTokenizer.from_pretrained("gpt2")

# 设置所有参数可训练
for param in model.parameters():
param.requires_grad = True

# 训练循环
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)

for epoch in range(num_epochs):
for batch in dataloader:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
optimizer.zero_grad()

优势: - 理论上能达到最佳性能 - 模型可以完全适应目标任务

劣势: - 需要大量计算资源( GPU 内存、训练时间) - 容易过拟合 - 可能导致灾难性遗忘 - 每个任务需要保存完整模型副本

冻结微调( Frozen Fine-tuning)

冻结微调是指冻结预训练模型的大部分参数,只训练部分层(通常是顶层)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 冻结微调示例
model = AutoModelForCausalLM.from_pretrained("gpt2")

# 冻结所有参数
for param in model.parameters():
param.requires_grad = False

# 只训练顶层(例如最后 2 层)
for param in model.transformer.h[-2:].parameters():
param.requires_grad = True

# 或者只训练分类头
classifier = nn.Linear(model.config.n_embd, num_labels)
optimizer = torch.optim.AdamW(classifier.parameters(), lr=1e-4)

优势: - 大幅减少可训练参数 - 降低计算成本 - 保留预训练知识

劣势: - 性能可能不如全量微调 - 需要仔细选择解冻的层 - 灵活性较低

参数效率对比

假设模型有 个参数:

  • 全量微调:需要更新 个参数
  • 冻结微调:只更新 个参数(例如最后几层)
  • PEFT 方法:通常只更新 个参数(例如 LoRA 只更新 0.1%-1% 的参数)

LoRA:低秩适应

LoRA( Low-Rank Adaptation)是当前最流行的 PEFT 方法之一。基本思路:不直接更新原始权重矩阵,而是学习一个低秩分解的增量更新

LoRA 原理

对于预训练权重矩阵, LoRA 不直接更新,而是学习两个低秩矩阵,其中

前向传播时,实际使用的权重为:

其中 是低秩更新。

参数效率

  • 原始矩阵参数量:
  • LoRA 参数量:
  • 参数减少比例: 时,参数量大幅减少。例如, 时:
  • 原始参数量: 1,048,576
  • LoRA 参数量: 16,384
  • 减少比例:约 98.4%

LoRA 实现

问题背景:全量微调需要更新所有参数,成本高昂。 LoRA 通过低秩分解,只学习权重的增量更新,大幅减少可训练参数。

解决思路:不直接更新原始权重矩阵,而是学习两个低秩矩阵,使得。前向传播时使用,其中 是缩放因子。

设计考虑: - 矩阵 初始化为随机小值, 初始化为零,确保初始时 LoRA 更新为零 - Rank 控制低秩的维度,通常选择 4-32 - Alpha 控制 LoRA 更新的强度,通常设置为 rank 的倍数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import torch
import torch.nn as nn
import torch.nn.functional as F

class LoRALayer(nn.Module):
"""
LoRA 层实现:低秩适应

问题:如何高效地微调大模型?
解决:学习低秩分解的权重增量,而不是直接更新原始权重

原理: W = W_0 + Δ W = W_0 + BA
其中 B ∈ R^(out_features × r), A ∈ R^(r × in_features)
参数量: r × (out_features + in_features) << out_features × in_features
"""
def __init__(self, in_features, out_features, rank=8, alpha=16):
"""
Args:
in_features: 输入特征维度
out_features: 输出特征维度
rank: 低秩的秩 r,控制 LoRA 的容量(通常 4-32)
alpha: 缩放因子,控制 LoRA 更新的强度(通常为 rank 的倍数)
"""
super().__init__()
self.rank = rank
self.alpha = alpha

# 低秩矩阵 A 和 B
# A: [r, in_features],初始化为小的随机值
# 使用 Kaiming 初始化,缩放因子 0.02 使初始更新很小
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.02)

# B: [out_features, r],初始化为零
# 确保初始时 LoRA 输出为零,不影响原始模型行为
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))

# 缩放因子: alpha / rank
# 作用:控制 LoRA 更新的强度, alpha 越大, LoRA 影响越大
self.scaling = alpha / rank

def forward(self, x, original_weight):
"""
前向传播:计算 W_0 * x + (alpha/r) * B * A * x

Args:
x: 输入张量, shape: [batch_size, ..., in_features]
original_weight: 原始权重矩阵 W_0, shape: [out_features, in_features]

Returns:
输出张量, shape: [batch_size, ..., out_features]
"""
# 计算 LoRA 更新: BAx
# 步骤 1: x @ A^T -> [batch_size, ..., r]
# 注意: lora_A 是[r, in_features],需要转置
lora_output = F.linear(x, self.lora_A.t()) # x @ A^T

# 步骤 2: (x @ A^T) @ B -> [batch_size, ..., out_features]
# lora_B 是[out_features, r],不需要转置
lora_output = F.linear(lora_output, self.lora_B) # (x @ A^T) @ B

# 应用缩放因子
# 缩放的作用:控制 LoRA 更新的强度
# 当 alpha=rank 时, scaling=1;当 alpha=2*rank 时, scaling=2
lora_output = lora_output * self.scaling

# 原始输出: W_0 * x
original_output = F.linear(x, original_weight)

# 最终输出:原始输出 + LoRA 更新
return original_output + lora_output

class LoRALinear(nn.Module):
"""
带 LoRA 的线性层包装器

问题:如何将 LoRA 应用到现有的线性层?
解决:包装原始线性层,在前向传播时应用 LoRA 更新
"""
def __init__(self, linear_layer, rank=8, alpha=16):
"""
Args:
linear_layer: 原始的 nn.Linear 层
rank: LoRA 的秩
alpha: LoRA 的缩放因子
"""
super().__init__()
self.linear = linear_layer # 原始线性层(冻结)
self.lora = LoRALayer(
linear_layer.in_features,
linear_layer.out_features,
rank=rank,
alpha=alpha
)

def forward(self, x):
"""
前向传播:应用 LoRA 更新

Args:
x: 输入张量

Returns:
输出张量 = W_0 * x + LoRA_update
"""
return self.lora(x, self.linear.weight)

关键点解读: - 低秩分解:将 的权重矩阵分解为 两个矩阵,参数量从 减少到 - 初始化策略随机初始化,零初始化,确保初始时 LoRA 不影响模型 - 缩放因子 控制 LoRA 更新的强度, 越大, LoRA 的影响越大

设计权衡: - ✅ 优点:参数量大幅减少(通常<1%),可以合并到原始权重,推理速度不变 - ⚠️ 注意: rank 的选择很重要,太小可能表达能力不足,太大失去参数效率优势

常见问题: - Q: 为什么 B 初始化为零? A: 确保初始时 LoRA 输出为零,不影响预训练模型的行为 - Q: Rank 如何选择? A: 通常从 8 或 16 开始,根据任务复杂度调整。简单任务可以用 4,复杂任务可能需要 32 或 64 - Q: Alpha 如何设置? A: 通常设置为 rank 的倍数,如。经验法则:通常效果较好

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 将 LoRA 应用到模型的注意力层
class AttentionWithLoRA(nn.Module):
def __init__(self, attention_layer, rank=8, alpha=16):
super().__init__()
self.attention = attention_layer
# 只对 Q 、 K 、 V 投影应用 LoRA
self.q_proj_lora = LoRALinear(attention_layer.q_proj, rank, alpha)
self.k_proj_lora = LoRALinear(attention_layer.k_proj, rank, alpha)
self.v_proj_lora = LoRALinear(attention_layer.v_proj, rank, alpha)

def forward(self, x):
# 使用 LoRA 版本的投影层
q = self.q_proj_lora(x)
k = self.k_proj_lora(x)
v = self.v_proj_lora(x)
# ... 注意力计算 ...

LoRA 的优势

  1. 参数高效:只更新少量参数(通常 <1%)
  2. 模块化:可以轻松添加或移除 LoRA 适配器
  3. 多任务:可以为不同任务训练不同的 LoRA 适配器
  4. 性能接近全量微调:在大多数任务上能达到 90%+ 的性能

LoRA 的超参数选择

  • rank (r):低秩的秩,通常选择 4 、 8 、 16 、 32 。 rank 越大,表达能力越强,但参数也越多。
  • alpha:缩放因子,通常设置为 rank 的倍数(如 rank=8, alpha=16)。 alpha 越大, LoRA 更新的影响越大。

经验法则:alpha = 2 * rank 通常效果较好。

QLoRA:量化 LoRA

QLoRA( Quantized LoRA)结合了量化和 LoRA,进一步降低了内存需求。

QLoRA 原理

QLoRA 的核心创新:

  1. 4-bit 量化:将模型权重量化为 4-bit
  2. NF4 量化:使用 NormalFloat4 量化格式
  3. 双量化:对量化常数再次量化
  4. 分页优化器:使用分页 AdamW 优化器

内存节省

  • FP16 全量微调:每个参数 2 bytes
  • QLoRA:每个参数约 0.5 bytes( 4-bit)+ LoRA 参数

对于 7B 模型: - FP16 全量微调:约 14 GB - QLoRA:约 3-4 GB

QLoRA 实现(使用 PEFT)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import AutoModelForCausalLM, AutoTokenizer

# 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)

# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto"
)

# 准备模型进行 k-bit 训练
model = prepare_model_for_kbit_training(model)

# LoRA 配置
lora_config = LoraConfig(
r=16, # rank
lora_alpha=32, # alpha
target_modules=["q_proj", "v_proj"], # 目标模块
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)

# 应用 LoRA
model = get_peft_model(model, lora_config)

# 训练
# 只有 LoRA 参数会被更新

Adapter 技术

Adapter 是在 Transformer 层中插入小型可训练模块的方法。

Adapter 架构

在每个 Transformer 层中添加两个小的前馈网络( Adapter):

  1. Down-projection:将隐藏维度降到较小的维度

  2. Up-projection:将维度恢复到原始隐藏维度

其中: -Double exponent: use braces to clarify: ^d ^{d_{adapter}} -

Adapter 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Adapter(nn.Module):
"""Adapter 模块"""
def __init__(self, hidden_size, adapter_size=64):
super().__init__()
self.adapter_size = adapter_size

# Down-projection
self.down_proj = nn.Linear(hidden_size, adapter_size)
# Up-projection
self.up_proj = nn.Linear(adapter_size, hidden_size)

# 初始化: up_proj 初始化为零,确保初始时 Adapter 不影响输出
nn.init.zeros_(self.up_proj.weight)
nn.init.zeros_(self.up_proj.bias)

def forward(self, x):
# 残差连接
return x + self.up_proj(F.relu(self.down_proj(x)))

class TransformerLayerWithAdapter(nn.Module):
"""带 Adapter 的 Transformer 层"""
def __init__(self, transformer_layer, adapter_size=64):
super().__init__()
self.transformer_layer = transformer_layer
self.adapter = Adapter(
transformer_layer.self_attn.embed_dim,
adapter_size
)

def forward(self, x, **kwargs):
# 原始 Transformer 层
x = self.transformer_layer(x, **kwargs)[0]
# 添加 Adapter
x = self.adapter(x)
return (x,)

Adapter vs LoRA

特性 Adapter LoRA
插入位置 Transformer 层内部 权重矩阵旁
参数量 中等(每个 Adapter ~0.5%) 较少(通常 <1%)
推理速度 略慢(额外前向计算) 较快(可合并到权重)
灵活性 中等 高(易于组合)

Prefix-Tuning

Prefix-Tuning 通过在输入序列前添加可学习的连续前缀( prefix)来适应任务。

Prefix-Tuning 原理

对于输入序列, Prefix-Tuning 添加可学习的前缀,形成:

这些前缀向量 是可训练的参数,而原始模型参数保持冻结。

注意力计算

在注意力机制中,前缀参与 key 和 value 的计算:

其中 包含前缀部分:

Prefix-Tuning 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class PrefixTuning(nn.Module):
"""Prefix-Tuning 实现"""
def __init__(self, config, num_prefix_tokens=10):
super().__init__()
self.num_prefix_tokens = num_prefix_tokens
self.hidden_size = config.hidden_size
self.num_heads = config.num_attention_heads
self.head_dim = self.hidden_size // self.num_heads

# 前缀参数
self.prefix_embeddings = nn.Parameter(
torch.randn(num_prefix_tokens, self.hidden_size)
)

# 用于生成 key 和 value 的前缀
self.prefix_key = nn.Linear(self.hidden_size, self.hidden_size)
self.prefix_value = nn.Linear(self.hidden_size, self.hidden_size)

def get_prefix_kv(self):
"""获取前缀的 key 和 value"""
prefix_k = self.prefix_key(self.prefix_embeddings)
prefix_v = self.prefix_value(self.prefix_embeddings)

# 重塑为多头格式
batch_size = 1 # 可以广播
prefix_k = prefix_k.view(
batch_size, self.num_prefix_tokens, self.num_heads, self.head_dim
).transpose(1, 2)
prefix_v = prefix_v.view(
batch_size, self.num_prefix_tokens, self.num_heads, self.head_dim
).transpose(1, 2)

return prefix_k, prefix_v

P-Tuning v2

P-Tuning v2 是 Prefix-Tuning 的改进版本,主要改进:

  1. 应用到所有层:不仅在输入层,在所有 Transformer 层都添加前缀
  2. 移除重参数化:直接优化前缀参数,不使用 MLP 重参数化
  3. 多任务学习:支持多任务前缀

P-Tuning v2 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PTuningV2(nn.Module):
"""P-Tuning v2 实现"""
def __init__(self, config, num_layers, num_prefix_tokens=20):
super().__init__()
self.num_layers = num_layers
self.num_prefix_tokens = num_prefix_tokens
self.hidden_size = config.hidden_size

# 为每一层创建前缀
self.prefix_embeddings = nn.ModuleList([
nn.Parameter(torch.randn(num_prefix_tokens, self.hidden_size))
for _ in range(num_layers)
])

def get_layer_prefix(self, layer_idx):
"""获取指定层的前缀"""
return self.prefix_embeddings[layer_idx]

指令微调( Instruction Tuning)

指令微调是让模型遵循指令的关键技术。通过在指令-响应对上微调,模型学会理解和执行各种指令。

指令数据格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
instruction_data = [
{
"instruction": "解释什么是机器学习",
"input": "",
"output": "机器学习是人工智能的一个分支,它使计算机能够从数据中学习..."
},
{
"instruction": "将以下英文翻译成中文",
"input": "Hello, how are you?",
"output": "你好,你好吗?"
},
{
"instruction": "总结以下文章",
"input": "[文章内容]",
"output": "[摘要]"
}
]

指令微调实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from transformers import Trainer, TrainingArguments

def format_instruction(example):
"""格式化指令数据"""
if example['input']:
prompt = f"指令:{example['instruction']}\n 输入:{example['input']}\n 输出:"
else:
prompt = f"指令:{example['instruction']}\n 输出:"

return {
"text": prompt + example['output']
}

# 准备数据
dataset = dataset.map(format_instruction)

# 训练参数
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_steps=500,
)

# 使用 LoRA 进行指令微调
from peft import LoraConfig, get_peft_model

lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.1,
task_type="CAUSAL_LM"
)

model = get_peft_model(model, lora_config)

trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
)

trainer.train()

RLHF 与对齐技术

RLHF( Reinforcement Learning from Human Feedback)是让模型与人类价值观对齐的重要技术。

RLHF 流程

RLHF 通常包含三个阶段:

  1. 监督微调( SFT):在指令数据上微调基础模型
  2. 奖励模型训练:训练一个奖励模型来评估输出质量
  3. 强化学习优化:使用 PPO 等算法优化策略模型

奖励模型训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class RewardModel(nn.Module):
"""奖励模型"""
def __init__(self, base_model):
super().__init__()
self.base_model = base_model
self.reward_head = nn.Linear(base_model.config.hidden_size, 1)

def forward(self, input_ids, attention_mask=None):
outputs = self.base_model(input_ids=input_ids, attention_mask=attention_mask)
# 使用最后一个 token 的隐藏状态
last_hidden_state = outputs.last_hidden_state[:, -1, :]
reward = self.reward_head(last_hidden_state)
return reward

# 训练奖励模型
def train_reward_model(model, chosen_data, rejected_data):
"""训练奖励模型"""
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)

for chosen, rejected in zip(chosen_data, rejected_data):
# 计算奖励
reward_chosen = model(chosen['input_ids'], chosen['attention_mask'])
reward_rejected = model(rejected['input_ids'], rejected['attention_mask'])

# 损失: chosen 的奖励应该大于 rejected
loss = -torch.log(torch.sigmoid(reward_chosen - reward_rejected))

loss.backward()
optimizer.step()
optimizer.zero_grad()

PPO 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from trl import PPOTrainer, PPOConfig

# PPO 配置
ppo_config = PPOConfig(
model_name="gpt2",
learning_rate=1e-5,
batch_size=4,
mini_batch_size=2,
gradient_accumulation_steps=4,
)

# PPO Trainer
ppo_trainer = PPOTrainer(
config=ppo_config,
model=model,
ref_model=ref_model, # 参考模型(冻结)
tokenizer=tokenizer,
reward_model=reward_model,
)

# 训练循环
for epoch in range(num_epochs):
for batch in dataloader:
# 生成响应
response = model.generate(**batch)

# 计算奖励
rewards = reward_model(response)

# PPO 更新
ppo_trainer.step(
queries=batch['input_ids'],
responses=response,
rewards=rewards
)

实战:使用 PEFT 微调大模型

完整示例:使用 LoRA 微调 LLaMA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import load_dataset
import torch

# 1. 加载模型和分词器
model_name = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

# 2. 加载模型(可选:使用量化)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)

# 3. 准备模型(如果使用量化)
# model = prepare_model_for_kbit_training(model)

# 4. 配置 LoRA
lora_config = LoraConfig(
r=16, # rank
lora_alpha=32, # alpha
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], # 目标模块
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)

# 5. 应用 LoRA
model = get_peft_model(model, lora_config)

# 打印可训练参数
model.print_trainable_parameters()
# 输出示例:
# trainable params: 4,194,304 || all params: 6,738,415,616 || trainable%: 0.06

# 6. 准备数据
def tokenize_function(examples):
return tokenizer(
examples["text"],
truncation=True,
max_length=512,
padding="max_length"
)

dataset = load_dataset("wikitext", "wikitext-2-raw-v1")
tokenized_dataset = dataset.map(tokenize_function, batched=True)

# 7. 训练参数
training_args = TrainingArguments(
output_dir="./llama-lora",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_steps=500,
evaluation_strategy="steps",
eval_steps=500,
)

# 8. 数据整理器
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False # 因果语言建模
)

# 9. Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"],
data_collator=data_collator,
)

# 10. 训练
trainer.train()

# 11. 保存模型
model.save_pretrained("./llama-lora-final")

使用 QLoRA 微调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)

# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto"
)

# 准备 k-bit 训练
model = prepare_model_for_kbit_training(model)

# LoRA 配置
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)

# 应用 LoRA
model = get_peft_model(model, lora_config)

# 训练(同上)

多任务 LoRA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 为不同任务训练不同的 LoRA 适配器
task1_lora = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
task_type="CAUSAL_LM"
)

task2_lora = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj", "k_proj"],
task_type="CAUSAL_LM"
)

# 训练任务 1
model_task1 = get_peft_model(base_model, task1_lora)
# ... 训练 ...

# 训练任务 2(可以加载不同的适配器)
model_task2 = get_peft_model(base_model, task2_lora)
# ... 训练 ...

# 推理时切换适配器
model.set_adapter("task1_adapter")
output1 = model.generate(...)

model.set_adapter("task2_adapter")
output2 = model.generate(...)

❓ Q&A: 模型微调与 PEFT 常见问题

Q1: 什么时候应该使用全量微调,什么时候使用 PEFT?

  • 全量微调:当你有充足的计算资源、大量高质量数据、且需要最佳性能时
  • PEFT:当计算资源有限、数据量中等、需要快速迭代或多任务适配时

Q2: LoRA 的 rank 如何选择?

  • 小任务/简单任务: rank=4 或 8
  • 中等任务: rank=16 或 32
  • 复杂任务: rank=32 或 64

建议:从 rank=16 开始,根据效果调整

Q3: QLoRA 和 LoRA 有什么区别?

  • LoRA:在 FP16/BF16 模型上应用
  • QLoRA:在 4-bit 量化模型上应用,内存需求更低

QLoRA 适合内存受限的场景。

Q4: 应该对哪些模块应用 LoRA?

通常选择注意力层的投影矩阵: - q_proj, k_proj, v_proj, o_proj( QKV 注意力) - gate_proj, up_proj, down_proj( MLP,可选)

建议:至少包含 q_proj 和 v_proj

Q5: PEFT 会影响推理速度吗?

  • LoRA:可以合并到原始权重,推理速度不变
  • Adapter:需要额外计算,推理略慢
  • Prefix-Tuning:需要处理额外 token,推理略慢

Q6: 如何选择 PEFT 方法?

方法 适用场景
LoRA 通用场景,平衡性能和效率
QLoRA 内存受限,大模型
Adapter 需要模块化设计
Prefix-Tuning 生成任务,需要控制生成

Q7: 指令微调需要多少数据?

  • 最少: 100-1000 条高质量指令
  • 推荐: 1000-10000 条
  • 最佳: 10000+ 条多样化指令

质量比数量更重要。

Q8: RLHF 是必需的吗?

不是。 RLHF 主要用于: - 需要与人类价值观对齐 - 需要控制输出风格 - 需要减少有害内容

对于大多数任务,指令微调已经足够。

Q9: 如何评估微调效果?

  • 任务指标:准确率、 F1 、 BLEU 等
  • 生成质量:人工评估、 GPT-4 评估
  • 对齐度:遵循指令的能力
  • 效率:参数量、推理速度

Q10: PEFT 的未来发展方向?

  • 更高效的参数利用:用更少参数达到更好效果
  • 自动化方法选择:自动选择最优 PEFT 配置
  • 多模态扩展:应用到视觉、语音等模态
  • 组合方法:结合多种 PEFT 技术
  • 本文标题:自然语言处理(八)—— 模型微调与 PEFT
  • 本文作者:Chen Kai
  • 创建时间:2024-03-15 16:15:00
  • 本文链接:https://www.chenk.top/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86%EF%BC%88%E5%85%AB%EF%BC%89%E2%80%94%E2%80%94-%E6%A8%A1%E5%9E%8B%E5%BE%AE%E8%B0%83%E4%B8%8EPEFT/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论