迁移学习(九)—— 参数高效微调
Chen Kai BOSS

当 GPT-3 有 1750 亿参数时,如何用单张 GPU 微调它?当需要为 100 个不同任务定制模型时,如何避免存储 100 份完整参数?参数高效微调( Parameter-Efficient Fine-Tuning, PEFT)给出了答案:只更新模型的一小部分参数,就能达到全量微调的效果。

本文从低秩适应的数学原理出发,系统讲解 LoRA 、 Adapter 、 Prefix-Tuning 等主流 PEFT 方法的设计哲学与实现细节,深入分析参数效率、计算开销与性能权衡,并提供从零实现 LoRA 的完整代码( 200+行)。

参数高效微调的动机

全量微调的困境

传统迁移学习采用全量微调( Full Fine-Tuning):

其中 包含模型的所有参数。

问题

  1. 内存爆炸: GPT-3( 175B 参数)全量微调需要 显存( FP32)
  2. 存储成本:为每个任务存储一份完整模型副本, 100 个任务需要 70TB
  3. 计算低效:即使只微调最后几层,仍需前向传播整个网络
  4. 灾难性遗忘:大幅更新参数容易破坏预训练知识

参数高效微调的核心思想

假设:预训练模型已学到通用表示,任务适配只需调整少量参数。

形式化为:

其中 是任务特定的参数增量,满足:

PEFT 的目标:只优化 ,冻结

参数效率的定义

参数效率定义为可训练参数占比:

典型 PEFT 方法的效率:

方法 可训练参数 效率
全量微调 100% 0%
BitFit ~0.1% 99.9%
Adapter ~0.5-2% 98-99.5%
LoRA ~0.1-1% 99-99.9%
Prefix-Tuning ~0.1% 99.9%

LoRA: 低秩适应

LoRA 的数学原理

LoRA( Low-Rank Adaptation)1的核心洞察:

假设:预训练权重矩阵的更新 具有低秩结构。

形式化为:

$$

W' = W_0 + W = W_0 + BA $$

其中:

  • 是预训练权重(冻结)
  • 是低秩分解(可训练)
  • 是秩(典型值: 1-64)

参数量对比:

  • 原始矩阵: 个参数
  • LoRA 增量: 个参数
  • 参数比例:

示例,参数比例为

为什么低秩假设成立?

内在维度理论

Aghajanyan 等人2证明:神经网络的学习过程发生在低维子空间中。

设模型参数为 ,存在低维投影 ),使得:

可以在 空间中优化,而非 空间。

经验验证

对预训练模型的权重矩阵进行奇异值分解:

$$

W = U V^T $$

观察奇异值分布:前几个奇异值远大于其余,表明权重矩阵接近低秩。

LoRA 的实现细节

初始化策略

  • 使用高斯初始化:
  • 初始化为零: 这样训练开始时 ,模型行为与预训练模型一致。

缩放因子

为了控制更新幅度,引入缩放因子

$$

W' = W_0 + BA $$

其中 是超参数(典型值:,即缩放因子为 1)。

应用位置

在 Transformer 中, LoRA 通常应用于:

  1. Query 和 Value 投影, (推荐)
  2. 所有线性层, , , , FFN(性能最佳)
  3. 仅 Value 投影(最轻量)

前向传播

$$

h = W_0 x + BAx $$

计算顺序:,避免显式构建 (节省内存)。

推理时的合并

训练完成后,可以将 LoRA 权重合并到原始权重:

$$

W_{} = W_0 + BA $$

推理时无额外计算开销,与全量微调模型等价。

LoRA 的优势与局限

优势

  1. 显存友好:只需存储 的梯度,显存需求降低至原来的 $$2. 模块化:不同任务的 可以独立存储和切换
  2. 无推理延迟:合并后与全量微调完全等价
  3. 训练加速:少量参数意味着更快的梯度计算

局限

  1. 秩的选择 太小性能受限, 太大失去效率优势
  2. 不适用所有层:在 embedding 层或输出层应用效果有限
  3. 理论保证不足:低秩假设在某些任务上可能不成立

Adapter: 瓶颈结构

Adapter 的设计

Adapter3在 Transformer 的每一层插入小型瓶颈模块:

其中: - 是输入特征 - 是下投影(降维) - 是上投影(升维) - 是非线性激活(如 ReLU 或 GELU) - 是瓶颈维度(典型值: 64)

参数量:(假设 bias 可忽略)。

Adapter 的插入位置

在 Transformer Block 中, Adapter 通常插入在两个位置:

  1. Multi-Head Attention 后

    1
    2
    3
    h = h + Attention(h)
    h = h + Adapter(LayerNorm(h))
    h = h + FFN(LayerNorm(h))

  2. Feed-Forward Network 后

    1
    2
    3
    h = h + Attention(LayerNorm(h))
    h = h + FFN(LayerNorm(h))
    h = h + Adapter(LayerNorm(h))

双插入版本(串行 Adapter):

1
2
h = h + Adapter ₁(Attention(h))
h = h + Adapter ₂(FFN(h))

并行 Adapter

为了减少推理延迟, He 等人4提出并行 Adapter:

$$

h' = h + (h) + (h) $$

Adapter 与 FFN 并行计算,避免串行依赖。

Adapter vs LoRA

维度 Adapter LoRA
参数位置 新增模块 修改原有权重
推理延迟 有(串行) 无(可合并)
训练稳定性 中等
实现复杂度 中等
适用场景 编码器模型( BERT) 生成模型( GPT)

Prefix-Tuning: 软提示优化

Prefix-Tuning 的核心思想

Prefix-Tuning5不修改模型参数,而是在输入序列前添加可训练的"虚拟 token"。

形式化为:

$$

P = {p_1, p_2, , p_m} ^{m d} $$

其中 是前缀长度(典型值: 10-100), 是隐藏维度。

前向传播:

$$

h = ([P; X]) $$

只有 是可训练的,模型参数全部冻结。

Prefix 的参数化

直接优化(不稳定)

直接优化 容易导致训练不稳定。

重参数化(推荐)

使用 MLP 将低维向量映射到高维:

$$

P = (P_{}) $$

其中 (如 )。

训练时优化 ,推理时只保留

Prefix-Tuning vs Prompt-Tuning

方法 Prefix-Tuning Prompt-Tuning
插入位置 每一层 仅输入层
参数量
性能 更好 适中
适用模型 编码器+解码器 仅解码器

P-Tuning v2

P-Tuning v26将 Prefix-Tuning 扩展到每一层的 Key 和 Value:

每层都有独立的前缀 , ,性能显著提升。

Prompt-Tuning: 纯软提示

Prompt-Tuning 的简化设计

Prompt-Tuning7进一步简化,只在输入层添加软提示:

可训练参数:,仅 个参数。

初始化策略

  1. 随机初始化2. 词嵌入初始化:从词汇表中选择相关词的嵌入
  2. 类别标签初始化:使用类别名的嵌入

实验表明:对于大模型(>10B 参数),初始化策略影响不大;小模型对初始化敏感。

长度的影响

提示长度 与性能的关系:

  • 小模型(<1B): 越大越好,通常需要
  • 大模型(>10B): 即可达到良好效果

原因:大模型的表达能力强,少量提示足以引导行为。

Prompt-Tuning 的理论解释

从优化角度, Prompt-Tuning 等价于在输入空间中寻找最优扰动:

$$

P^{*} = _P (f([P; X]), y) $$

这是一种输入空间优化,而非参数空间优化。

BitFit: 仅偏置微调

BitFit 的极简主义

BitFit8提出了极端简化的 PEFT:仅微调偏置项

在 Transformer 中,所有线性层都有偏置

$$

y = Wx + b $$

BitFit 冻结 ,只优化

参数量:假设每层有 个偏置( Query, Key, Value, Output 各 个), 层模型共 个参数,占比 ~0.1%。

为什么仅偏置有效?

偏置的特殊性

偏置可以理解为任务特定的全局偏移

$$

y = W(x + W^{-1}b) $$

等价于对输入施加偏移

经验证据

实验表明: BitFit 在少样本场景下与全量微调性能接近(尤其是大模型)。

原因:预训练模型的权重已经编码了通用知识,偏置的调整足以适配新任务。

BitFit 的局限

  1. 小模型效果差:<1B 参数的模型, BitFit 显著弱于其他 PEFT 方法
  2. 复杂任务受限:需要大幅改变特征表示的任务(如领域迁移), BitFit 力不从心
  3. 无法利用低秩结构:偏置是向量,无法像 LoRA 那样利用低秩假设

(IA)³: 激活缩放

(IA)³的设计

(IA)³( Infused Adapter by Inhibiting and Amplifying Inner Activations)9通过缩放激活来适配任务:

$$

h' = h l_v $$

其中 是逐元素乘法, 是可训练的缩放向量(初始化为 1)。

在 Transformer 中,应用于三个位置:

  1. Attention 的 Key 和 Value: $$

K' = (W_K x) l_k, V' = (W_V x) l_v

h_{} ' = (W_1 h) l_{} $$

参数量:每层 个参数($ l_k, l_v, l_{} L$ 层共 ,占比 ~0.01%。

(IA)³的优势

  1. 极致高效:参数量比 LoRA 还少一个数量级
  2. 无推理延迟:缩放操作几乎无开销
  3. 数值稳定:初始化为 1,训练过程平滑

缩放的直觉

缩放可以理解为特征选择

  • :放大第 维特征,增强其重要性
  • :抑制第 维特征,减弱其影响
  • :近似移除该维度

通过学习缩放模式,模型可以为不同任务调整特征的相对重要性。

完整代码实现:从零实现 LoRA

下面实现一个完整的 LoRA 模块,包括线性层的 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
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
"""
从零实现 LoRA:低秩适应的参数高效微调
包含: LoRA 层、 LoRA 模型、训练、推理、权重合并
"""

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
from typing import Optional, List

# 设置随机种子
torch.manual_seed(42)
np.random.seed(42)

# ============================================================================
# LoRA 层实现
# ============================================================================

class LoRALayer(nn.Module):
"""
LoRA 层: W' = W_0 + (α/r) * BA
"""
def __init__(
self,
in_features: int,
out_features: int,
rank: int = 4,
alpha: float = 1.0,
dropout: float = 0.0
):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.rank = rank
self.alpha = alpha

# 预训练权重(冻结)
self.weight = nn.Parameter(torch.randn(out_features, in_features))
self.weight.requires_grad = False

# 偏置(冻结)
self.bias = nn.Parameter(torch.zeros(out_features))
self.bias.requires_grad = False

# LoRA 低秩矩阵
self.lora_A = nn.Parameter(torch.randn(rank, in_features))
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))

# Dropout
self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()

# 初始化
nn.init.kaiming_uniform_(self.lora_A, a=np.sqrt(5))
nn.init.zeros_(self.lora_B)

# 缩放因子
self.scaling = alpha / rank

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
前向传播: h = W_0 x + (α/r) BA x
"""
# 原始线性变换(冻结)
result = nn.functional.linear(x, self.weight, self.bias)

# LoRA 增量:先 A 后 B,避免显式构建 BA
lora_out = (self.dropout(x) @ self.lora_A.t()) @ self.lora_B.t()
lora_out = lora_out * self.scaling

return result + lora_out

def merge_weights(self):
"""
合并 LoRA 权重到原始权重: W_merged = W_0 + (α/r) BA
"""
if self.rank > 0:
delta_W = self.lora_B @ self.lora_A * self.scaling
self.weight.data += delta_W
# 清空 LoRA 矩阵
self.lora_A.data.zero_()
self.lora_B.data.zero_()

def extra_repr(self) -> str:
return f'in_features={self.in_features}, out_features={self.out_features}, rank={self.rank}, alpha={self.alpha}'

# ============================================================================
# LoRA 应用到模型
# ============================================================================

def apply_lora_to_linear(model: nn.Module, rank: int = 4, alpha: float = 1.0,
target_modules: Optional[List[str]] = None):
"""
将模型中的 nn.Linear 替换为 LoRALayer
Args:
model: 目标模型
rank: LoRA 秩
alpha: 缩放因子
target_modules: 要替换的模块名列表(如['query', 'value'])
"""
for name, module in model.named_children():
if isinstance(module, nn.Linear):
# 检查是否在目标模块中
if target_modules is None or any(target in name for target in target_modules):
# 创建 LoRA 层
lora_layer = LoRALayer(
in_features=module.in_features,
out_features=module.out_features,
rank=rank,
alpha=alpha
)
# 复制预训练权重
lora_layer.weight.data = module.weight.data.clone()
if module.bias is not None:
lora_layer.bias.data = module.bias.data.clone()

# 替换模块
setattr(model, name, lora_layer)
print(f"Applied LoRA to {name}: {module.in_features} -> {module.out_features}, rank={rank}")
else:
# 递归应用到子模块
apply_lora_to_linear(module, rank, alpha, target_modules)

def count_parameters(model: nn.Module, trainable_only: bool = False) -> int:
"""
统计模型参数量
"""
if trainable_only:
return sum(p.numel() for p in model.parameters() if p.requires_grad)
else:
return sum(p.numel() for p in model.parameters())

# ============================================================================
# 示例模型:简单的 Transformer Block
# ============================================================================

class MultiHeadAttention(nn.Module):
"""
简化的多头注意力
"""
def __init__(self, d_model: int, num_heads: int):
super().__init__()
assert d_model % num_heads == 0
self.d_model = d_model
self.num_heads = num_heads
self.head_dim = d_model // num_heads

# QKV 投影
self.query = nn.Linear(d_model, d_model)
self.key = nn.Linear(d_model, d_model)
self.value = nn.Linear(d_model, d_model)
self.out = nn.Linear(d_model, d_model)

def forward(self, x: torch.Tensor) -> torch.Tensor:
batch_size, seq_len, d_model = x.shape

# QKV 投影
Q = self.query(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
K = self.key(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = self.value(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)

# 注意力得分
scores = Q @ K.transpose(-2, -1) / np.sqrt(self.head_dim)
attn = torch.softmax(scores, dim=-1)

# 加权求和
out = attn @ V
out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)

return self.out(out)

class FeedForward(nn.Module):
"""
Feed-Forward Network
"""
def __init__(self, d_model: int, d_ff: int):
super().__init__()
self.fc1 = nn.Linear(d_model, d_ff)
self.fc2 = nn.Linear(d_ff, d_model)
self.activation = nn.GELU()

def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.fc2(self.activation(self.fc1(x)))

class TransformerBlock(nn.Module):
"""
简化的 Transformer Block
"""
def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.attention = MultiHeadAttention(d_model, num_heads)
self.ffn = FeedForward(d_model, d_ff)

self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)

self.dropout = nn.Dropout(dropout)

def forward(self, x: torch.Tensor) -> torch.Tensor:
# Self-attention
attn_out = self.attention(self.norm1(x))
x = x + self.dropout(attn_out)

# Feed-forward
ffn_out = self.ffn(self.norm2(x))
x = x + self.dropout(ffn_out)

return x

class SimpleTransformer(nn.Module):
"""
简单的 Transformer 模型(用于演示 LoRA)
"""
def __init__(self, d_model: int = 256, num_heads: int = 4, num_layers: int = 4,
d_ff: int = 1024, vocab_size: int = 10000, num_classes: int = 10):
super().__init__()
self.embedding = nn.Embedding(vocab_size, d_model)
self.blocks = nn.ModuleList([
TransformerBlock(d_model, num_heads, d_ff) for _ in range(num_layers)
])
self.classifier = nn.Linear(d_model, num_classes)

def forward(self, x: torch.Tensor) -> torch.Tensor:
# Embedding
x = self.embedding(x) # (B, L) -> (B, L, D)

# Transformer blocks
for block in self.blocks:
x = block(x)

# 取平均池化
x = x.mean(dim=1) # (B, L, D) -> (B, D)

# 分类
return self.classifier(x)

# ============================================================================
# 模拟数据集
# ============================================================================

class SyntheticTextDataset(Dataset):
"""
模拟文本分类数据集
"""
def __init__(self, num_samples: int = 1000, seq_len: int = 32,
vocab_size: int = 10000, num_classes: int = 10):
self.num_samples = num_samples
self.seq_len = seq_len

# 生成随机数据
self.data = torch.randint(1, vocab_size, (num_samples, seq_len))
self.labels = torch.randint(0, num_classes, (num_samples,))

def __len__(self):
return self.num_samples

def __getitem__(self, idx):
return self.data[idx], self.labels[idx]

# ============================================================================
# 训练函数
# ============================================================================

def train_model(model, dataloader, optimizer, criterion, device, num_epochs=10):
"""
训练模型
"""
model.train()
losses = []
accuracies = []

for epoch in range(num_epochs):
epoch_loss = 0
epoch_correct = 0
epoch_total = 0

for batch_idx, (inputs, labels) in enumerate(dataloader):
inputs = inputs.to(device)
labels = labels.to(device)

# 前向传播
outputs = model(inputs)
loss = criterion(outputs, labels)

# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()

# 统计
epoch_loss += loss.item()
_, predicted = torch.max(outputs, 1)
epoch_correct += (predicted == labels).sum().item()
epoch_total += labels.size(0)

if (batch_idx + 1) % 10 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Batch [{batch_idx+1}/{len(dataloader)}], Loss: {loss.item():.4f}")

avg_loss = epoch_loss / len(dataloader)
accuracy = 100 * epoch_correct / epoch_total
losses.append(avg_loss)
accuracies.append(accuracy)

print(f"Epoch [{epoch+1}/{num_epochs}] Loss: {avg_loss:.4f}, Accuracy: {accuracy:.2f}%")

return losses, accuracies

# ============================================================================
# 可视化
# ============================================================================

def plot_training_curves(losses_baseline, accuracies_baseline,
losses_lora, accuracies_lora):
"""
绘制训练曲线对比
"""
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 损失曲线
axes[0].plot(losses_baseline, marker='o', label='Full Fine-Tuning', linewidth=2)
axes[0].plot(losses_lora, marker='s', label='LoRA', linewidth=2)
axes[0].set_xlabel('Epoch', fontsize=12)
axes[0].set_ylabel('Loss', fontsize=12)
axes[0].set_title('Training Loss Comparison', fontsize=14, fontweight='bold')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 准确率曲线
axes[1].plot(accuracies_baseline, marker='o', label='Full Fine-Tuning', linewidth=2)
axes[1].plot(accuracies_lora, marker='s', label='LoRA', linewidth=2)
axes[1].set_xlabel('Epoch', fontsize=12)
axes[1].set_ylabel('Accuracy (%)', fontsize=12)
axes[1].set_title('Training Accuracy Comparison', fontsize=14, fontweight='bold')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('lora_training_comparison.png', dpi=150, bbox_inches='tight')
plt.close()
print("Training curves saved to lora_training_comparison.png")

def visualize_lora_matrices(model):
"""
可视化 LoRA 矩阵的奇异值分布
"""
lora_layers = [m for m in model.modules() if isinstance(m, LoRALayer)]

if not lora_layers:
print("No LoRA layers found")
return

fig, axes = plt.subplots(2, 2, figsize=(12, 10))
axes = axes.flatten()

for idx, layer in enumerate(lora_layers[:4]):
# 计算 BA 的奇异值
BA = layer.lora_B @ layer.lora_A
U, S, V = torch.svd(BA.detach().cpu())

axes[idx].bar(range(len(S)), S.numpy())
axes[idx].set_xlabel('Singular Value Index', fontsize=10)
axes[idx].set_ylabel('Magnitude', fontsize=10)
axes[idx].set_title(f'LoRA Layer {idx+1}: Singular Values of BA', fontsize=12)
axes[idx].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('lora_singular_values.png', dpi=150, bbox_inches='tight')
plt.close()
print("Singular value plots saved to lora_singular_values.png")

# ============================================================================
# 主函数
# ============================================================================

def main():
# 超参数
d_model = 256
num_heads = 4
num_layers = 4
d_ff = 1024
vocab_size = 10000
num_classes = 10
batch_size = 32
num_epochs = 20
learning_rate = 1e-3
lora_rank = 8
lora_alpha = 16

# 设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")

# 创建数据集
print("\nCreating dataset...")
dataset = SyntheticTextDataset(num_samples=1000, vocab_size=vocab_size, num_classes=num_classes)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# ========================================================================
# 方法 1:全量微调( baseline)
# ========================================================================
print("\n" + "="*60)
print("Method 1: Full Fine-Tuning (Baseline)")
print("="*60)

model_baseline = SimpleTransformer(
d_model=d_model, num_heads=num_heads, num_layers=num_layers,
d_ff=d_ff, vocab_size=vocab_size, num_classes=num_classes
).to(device)

total_params_baseline = count_parameters(model_baseline)
trainable_params_baseline = count_parameters(model_baseline, trainable_only=True)
print(f"Total parameters: {total_params_baseline:,}")
print(f"Trainable parameters: {trainable_params_baseline:,} ({100*trainable_params_baseline/total_params_baseline:.2f}%)")

optimizer_baseline = optim.Adam(model_baseline.parameters(), lr=learning_rate)
criterion = nn.CrossEntropyLoss()

losses_baseline, accuracies_baseline = train_model(
model_baseline, dataloader, optimizer_baseline, criterion, device, num_epochs
)

# ========================================================================
# 方法 2: LoRA 微调
# ========================================================================
print("\n" + "="*60)
print("Method 2: LoRA Fine-Tuning")
print("="*60)

model_lora = SimpleTransformer(
d_model=d_model, num_heads=num_heads, num_layers=num_layers,
d_ff=d_ff, vocab_size=vocab_size, num_classes=num_classes
).to(device)

# 应用 LoRA 到 Query 和 Value 投影
apply_lora_to_linear(model_lora, rank=lora_rank, alpha=lora_alpha,
target_modules=['query', 'value'])

total_params_lora = count_parameters(model_lora)
trainable_params_lora = count_parameters(model_lora, trainable_only=True)
print(f"\nTotal parameters: {total_params_lora:,}")
print(f"Trainable parameters: {trainable_params_lora:,} ({100*trainable_params_lora/total_params_lora:.2f}%)")
print(f"Parameter reduction: {100*(trainable_params_baseline - trainable_params_lora)/trainable_params_baseline:.2f}%")

optimizer_lora = optim.Adam(
[p for p in model_lora.parameters() if p.requires_grad],
lr=learning_rate
)

losses_lora, accuracies_lora = train_model(
model_lora, dataloader, optimizer_lora, criterion, device, num_epochs
)

# ========================================================================
# 结果对比
# ========================================================================
print("\n" + "="*60)
print("Results Comparison")
print("="*60)
print(f"Full Fine-Tuning - Final Loss: {losses_baseline[-1]:.4f}, Final Accuracy: {accuracies_baseline[-1]:.2f}%")
print(f"LoRA Fine-Tuning - Final Loss: {losses_lora[-1]:.4f}, Final Accuracy: {accuracies_lora[-1]:.2f}%")
print(f"Performance gap: {accuracies_lora[-1] - accuracies_baseline[-1]:.2f}%")

# 绘制训练曲线
plot_training_curves(losses_baseline, accuracies_baseline, losses_lora, accuracies_lora)

# 可视化 LoRA 矩阵
visualize_lora_matrices(model_lora)

# ========================================================================
# 权重合并测试
# ========================================================================
print("\n" + "="*60)
print("Weight Merging Test")
print("="*60)

# 测试一个样本
test_input = torch.randint(1, vocab_size, (1, 32)).to(device)

# 合并前的输出
model_lora.eval()
with torch.no_grad():
output_before = model_lora(test_input)

# 合并权重
for module in model_lora.modules():
if isinstance(module, LoRALayer):
module.merge_weights()

# 合并后的输出
with torch.no_grad():
output_after = model_lora(test_input)

# 验证输出一致性
diff = torch.abs(output_before - output_after).max().item()
print(f"Max difference between outputs before and after merging: {diff:.8f}")
print("Weights successfully merged!" if diff < 1e-5 else "Warning: Outputs differ!")

print("\n" + "="*60)
print("Experiment completed!")
print("="*60)

if __name__ == "__main__":
main()

代码说明

核心组件

  1. LoRALayer:实现低秩分解 2. apply_lora_to_linear:自动替换模型中的 Linear 层
  2. 权重合并:训练后将 LoRA 权重合并到原始权重,无推理开销

实验设计

  1. 方法 1:全量微调( baseline)
  2. 方法 2: LoRA 微调( rank=8)
  3. 对比参数量、训练曲线、最终性能

关键细节

  • 初始化: 用 Kaiming, 全零
  • 计算顺序:,避免显式构建
  • 权重合并:推理时无额外开销

方法对比与选择指南

性能对比

在 GLUE 基准上的实验结果( RoBERTa-base,~125M 参数):

方法 可训练参数 平均得分 相对全量微调
全量微调 100% 84.8 100%
BitFit 0.1% 82.3 97.1%
Adapter 0.5% 84.2 99.3%
Prefix-Tuning 0.1% 83.9 99.0%
LoRA (r=8) 0.2% 84.6 99.8%
(IA)³ 0.01% 83.5 98.5%

结论: LoRA 在参数效率和性能之间取得最佳平衡。

适用场景

LoRA 适用于:

  • 生成模型( GPT 、 T5)
  • 大规模模型(>1B 参数)
  • 需要频繁切换任务
  • 显存受限

Adapter 适用于:

  • 编码器模型( BERT 、 RoBERTa)
  • 训练稳定性要求高
  • 推理延迟不敏感
  • 实现简单优先

Prefix-Tuning 适用于:

  • 生成任务(摘要、翻译)
  • 少样本学习
  • 提示工程结合
  • 输入长度可变

Prompt-Tuning 适用于:

  • 超大模型(>10B 参数)
  • 零样本/少样本场景
  • 输入格式灵活
  • 任务切换频繁

BitFit 适用于:

  • 大模型的快速原型
  • 极致参数效率需求
  • 简单任务
  • 计算资源极度受限

(IA)³适用于:

  • Few-shot 场景
  • 特征重要性调整
  • 快速适配
  • 与其他方法组合

组合策略

多种 PEFT 方法可以组合使用:

  1. LoRA + Adapter: LoRA 用于 attention, Adapter 用于 FFN
  2. Prefix-Tuning + LoRA:前缀调整输入, LoRA 调整权重
  3. BitFit + LoRA:偏置全微调,权重低秩微调

理论分析与未来方向

低秩假设的理论基础

神经正切核理论

在无限宽网络极限下,神经网络的训练动力学由神经正切核( NTK)描述:

$$

K(x, x') = [f(x; ) f(x'; )^T] $$

NTK 理论表明:在特定初始化下,权重更新 集中在低秩子空间。

信息瓶颈

从信息论角度,有效的特征表示应该最小化冗余:

低秩结构正是这种信息压缩的体现。

未来研究方向

  1. 自适应秩选择:根据任务自动确定最优秩 2. 结构化低秩:利用张量分解( Tucker 、 CP)进一步压缩
  2. 动态 PEFT:训练过程中动态调整参数效率
  3. 硬件友好设计:针对特定硬件( TPU 、 NPU)优化 PEFT 实现
  4. 多任务 PEFT:共享部分 LoRA 参数,学习任务间的相关性

常见问题解答

Q1: LoRA 的秩 如何选择?

经验规则:

  • 小模型(<1B):
  • 中等模型( 1B-10B):
  • 大模型(>10B): 原则
  • 任务复杂度高 → 更大的
  • 数据量充足 → 可以用更大的
  • 显存受限 → 减小 实践中,先用 测试,然后根据性能调整。

Q2: LoRA 应该应用到哪些层?

优先级(从高到低):

  1. Query 和 Value:影响 attention 机制,效果最显著
  2. 所有 attention 投影( QKVO):性能最佳,参数稍多
  3. FFN 层:与 attention 结合使用
  4. 仅 Value:最轻量,适合极端资源受限

建议:先尝试 Query+Value,性能不足再扩展到所有层。

Q3: LoRA 与全量微调的性能差距?

实验表明:

  • 大模型(>10B):差距 <1%
  • 中等模型( 1B-10B):差距 1-3%
  • 小模型(<1B):差距可能 >5%

原因:大模型的内在维度低,低秩假设更成立。

Q4: LoRA 训练时的学习率如何设置?

经验值:

  • LoRA 参数:
  • 通常比全量微调的学习率高 1-2 个数量级

原因: LoRA 参数从零初始化,需要更大的学习率快速学习。

Q5: 多任务场景如何管理 LoRA 参数?

策略:

  1. 独立存储:每个任务一组 ,推理时动态加载
  2. 共享基座:共享 ,任务特定 (或反之)
  3. 混合专家:多个 LoRA 模块,根据输入路由

示例: 100 个任务,每个 LoRA 10MB,总共 1GB( vs 全量微调需要 100 × 700GB)。

Q6: LoRA 是否会导致灾难性遗忘?

相比全量微调, LoRA 显著减轻灾难性遗忘:

  • 原因:预训练权重 完全冻结,不会被破坏
  • 增量 只编码任务特定知识

实验: LoRA 在连续学习场景下优于全量微调。

Q7: LoRA 的推理速度如何?

  • 合并前:略慢(~5%),因为需要额外计算
  • 合并后:与全量微调完全相同,零开销

建议:部署时合并权重,保持推理效率。

Q8: Adapter 和 LoRA 哪个更好?

取决于场景:

维度 Adapter 更优 LoRA 更优
模型类型 BERT 类编码器 GPT 类生成器
训练稳定性 稳定 需要调参
推理延迟 有延迟 无延迟(合并后)
实现复杂度 简单 中等
参数效率 中等

实践:先尝试 LoRA,不行再考虑 Adapter 。

Q9: PEFT 方法可以和量化结合吗?

可以!常见组合:

  1. QLoRA: 4-bit 量化 + LoRA,单 GPU 微调 65B 模型
  2. 量化 Adapter:量化基座模型,只 Adapter 用 FP16
  3. 混合精度 PEFT: LoRA 用 FP32,其他用 INT8

QLoRA 效果:显存需求降低 4 倍,性能下降 <2%。

Q10: Prefix-Tuning 为什么需要重参数化?

直接优化 的问题:

  1. 训练不稳定:梯度方差大
  2. 收敛慢:高维空间优化困难
  3. 过拟合:参数直接暴露给损失函数

重参数化()的好处:

  • MLP 提供正则化效果
  • 低维 更容易优化
  • 训练稳定性提升

Q11: PEFT 方法在 CV 任务上效果如何?

效果不如 NLP:

  • 原因:视觉模型的内在维度更高,低秩假设不够强
  • 改进:使用更大的秩 (如

最新进展: Convpass 、 SSF 等方法针对 CV 设计的 PEFT,效果接近全量微调。

Q12: 如何调试 PEFT 训练不收敛?

诊断步骤:

  1. 检查梯度: LoRA 参数的梯度是否正常?

    1
    2
    3
    for name, param in model.named_parameters():
    if param.requires_grad and param.grad is not None:
    print(f"{name}: grad_norm={param.grad.norm().item():.6f}")

  2. 增大学习率: LoRA 需要比全量微调更高的 lr

  3. 检查初始化 应为零, 应随机

  4. 增大秩 太小可能表达能力不足

  5. 移除 Dropout:某些情况下 LoRA 对 Dropout 敏感

小结

本文全面介绍了参数高效微调技术:

  1. LoRA:低秩分解的数学原理与完整实现
  2. Adapter:瓶颈结构的设计与应用
  3. Prefix-Tuning:软提示优化与重参数化
  4. Prompt-Tuning:纯软提示的极简设计
  5. BitFit:仅偏置微调的极致效率
  6. (IA)³:激活缩放的创新方法
  7. 方法对比:性能、效率、适用场景的全面分析
  8. 完整代码:从零实现 LoRA 的 200+行工程级代码

PEFT 技术让大模型微调从"奢侈品"变成"日用品",单张 GPU 就能微调百亿参数模型。下一章我们将探讨持续学习,看如何让模型在不忘记旧知识的前提下持续学习新任务。

参考文献


  1. Hu, E. J., Shen, Y., Wallis, P., et al. (2021). LoRA: Low-rank adaptation of large language models. ICLR.↩︎

  2. Aghajanyan, A., Gupta, S., & Zettlemoyer, L. (2020). Intrinsic dimensionality explains the effectiveness of language model fine-tuning. ACL.↩︎

  3. Houlsby, N., Giurgiu, A., Jastrzebski, S., et al. (2019). Parameter-efficient transfer learning for NLP. ICML.↩︎

  4. He, J., Zhou, C., Ma, X., et al. (2021). Towards a unified view of parameter-efficient transfer learning. ICLR.↩︎

  5. Li, X. L., & Liang, P. (2021). Prefix-tuning: Optimizing continuous prompts for generation. ACL.↩︎

  6. Liu, X., Ji, K., Fu, Y., et al. (2022). P-tuning v2: Prompt tuning can be comparable to fine-tuning universally across scales and tasks. ACL.↩︎

  7. Lester, B., Al-Rfou, R., & Constant, N. (2021). The power of scale for parameter-efficient prompt tuning. EMNLP.↩︎

  8. Zaken, E. B., Ravfogel, S., & Goldberg, Y. (2021). BitFit: Simple parameter-efficient fine-tuning for transformer-based masked language-models. ACL.↩︎

  9. Liu, H., Tam, D., Muqeeth, M., et al. (2022). Few-shot parameter-efficient fine-tuning is better and cheaper than in-context learning. NeurIPS.↩︎

  • 本文标题:迁移学习(九)—— 参数高效微调
  • 本文作者:Chen Kai
  • 创建时间:2024-12-21 09:15:00
  • 本文链接:https://www.chenk.top/%E8%BF%81%E7%A7%BB%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%B9%9D%EF%BC%89%E2%80%94%E2%80%94-%E5%8F%82%E6%95%B0%E9%AB%98%E6%95%88%E5%BE%AE%E8%B0%83/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论