参数高效微调:加速小模型训练,降低 GPU 成本
大家好!今天我们来聊聊如何利用参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)技术,提升小模型训练速度,并降低企业 GPU 成本压力。在深度学习领域,模型规模的增长似乎成了趋势。然而,对于许多企业来说,从头训练或全参数微调大型模型的成本是巨大的。幸运的是,PEFT 提供了一种更经济高效的解决方案。
1. 传统微调的局限性
传统微调方法需要更新模型的所有参数,这在以下几个方面带来了挑战:
- 计算成本高昂: 更新所有参数需要大量的 GPU 资源和时间,尤其是对于大型模型。
- 存储需求庞大: 需要存储完整模型的多个副本,例如原始模型、微调后的模型等。
- 容易过拟合: 在小数据集上微调大型模型时,容易出现过拟合现象。
为了解决这些问题,PEFT 技术应运而生。
2. 参数高效微调 (PEFT) 的核心思想
PEFT 的核心思想是在预训练模型的基础上,只微调少量参数,同时保持预训练模型的知识。 这样可以在保证模型性能的同时,显著降低计算成本和存储需求。
PEFT 方法主要分为以下几类:
- 添加少量可训练参数: 例如 Adapter、Prefix-Tuning、LoRA 等。
- 选择性微调: 例如 BitFit、(IA)^3等。
- 重参数化: 例如 AdaLoRA 等。
3. 几种主流的 PEFT 技术详解
接下来,我们详细介绍几种主流的 PEFT 技术,并提供相应的代码示例。
3.1 Adapter
Adapter 方法在预训练模型的每一层中插入少量可训练的模块(Adapter layers)。 这些 Adapter layers 通常由一个 bottleneck 结构组成,先将输入降维,然后经过非线性激活函数,最后再升维到原始维度。
原理: Adapter layers 允许模型学习特定于下游任务的知识,而无需修改预训练模型的原始参数。
优点:
- 易于实现和集成。
- 可以在不同的任务之间共享预训练模型。
- 可以灵活地调整 Adapter layers 的大小,以控制微调的参数量。
缺点:
- 需要手动设计 Adapter layers 的结构,例如 bottleneck 的大小。
- 可能会引入额外的推理延迟。
代码示例 (使用 PyTorch):
import torch
import torch.nn as nn
class Adapter(nn.Module):
def __init__(self, input_dim, bottleneck_dim):
super().__init__()
self.down = nn.Linear(input_dim, bottleneck_dim)
self.up = nn.Linear(bottleneck_dim, input_dim)
self.activation = nn.ReLU()
def forward(self, x):
residual = x
x = self.down(x)
x = self.activation(x)
x = self.up(x)
return x + residual
class ModelWithAdapter(nn.Module):
def __init__(self, base_model, bottleneck_dim):
super().__init__()
self.base_model = base_model
self.adapter1 = Adapter(base_model.config.hidden_size, bottleneck_dim)
self.adapter2 = Adapter(base_model.config.hidden_size, bottleneck_dim)
# 可以根据需要添加更多的 Adapter layers
def forward(self, input_ids, attention_mask):
outputs = self.base_model(input_ids, attention_mask=attention_mask)
hidden_states = outputs.last_hidden_state
hidden_states = self.adapter1(hidden_states)
hidden_states = self.adapter2(hidden_states)
# 可以根据需要将 Adapter layers 插入到模型的不同位置
return hidden_states
# 示例用法
from transformers import AutoModel
model_name = "bert-base-uncased"
base_model = AutoModel.from_pretrained(model_name)
bottleneck_dim = 64
model = ModelWithAdapter(base_model, bottleneck_dim)
# 只训练 Adapter 的参数
for name, param in model.named_parameters():
if "adapter" not in name:
param.requires_grad = False
# 计算可训练参数的数量
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"可训练参数数量: {trainable_params}")
3.2 Prefix-Tuning
Prefix-Tuning 在模型的每一层输入序列的前面添加一段可训练的 "prefix"。 这些 prefix 向量与输入序列一起传递到模型的后续层。
原理: Prefix 允许模型调整其内部状态,以适应下游任务,而无需修改预训练模型的原始参数。
优点:
- 可以有效地控制模型的行为。
- 可以在不同的任务之间共享预训练模型。
- 可以灵活地调整 prefix 的长度,以控制微调的参数量。
缺点:
- 可能会增加模型的计算复杂度,因为需要处理额外的 prefix 向量。
- prefix 的长度需要仔细调整,以避免影响模型性能。
代码示例 (使用 PyTorch):
import torch
import torch.nn as nn
class PrefixTuning(nn.Module):
def __init__(self, config, prefix_length):
super().__init__()
self.prefix_length = prefix_length
self.embedding = nn.Embedding(prefix_length, config.hidden_size)
self.config = config
def forward(self, hidden_states):
batch_size = hidden_states.shape[0]
prefix = self.embedding(torch.arange(self.prefix_length).to(hidden_states.device))
prefix = prefix.unsqueeze(0).expand(batch_size, -1, -1)
hidden_states = torch.cat((prefix, hidden_states), dim=1)
return hidden_states
class ModelWithPrefixTuning(nn.Module):
def __init__(self, base_model, prefix_length):
super().__init__()
self.base_model = base_model
self.prefix_tuning = PrefixTuning(base_model.config, prefix_length)
def forward(self, input_ids, attention_mask):
outputs = self.base_model(input_ids, attention_mask=attention_mask, output_hidden_states=True)
hidden_states = outputs.hidden_states[0] # Get the first layer's hidden states
hidden_states = self.prefix_tuning(hidden_states)
# Reconstruct the input for the base model with the modified hidden states
# Note: This is a simplified example and might require adjustments based on the model architecture.
extended_attention_mask = torch.cat((torch.ones((input_ids.shape[0], self.prefix_tuning.prefix_length)).to(input_ids.device), attention_mask), dim=1)
outputs = self.base_model(inputs_embeds=hidden_states, attention_mask=extended_attention_mask)
return outputs
# 示例用法
from transformers import AutoModel, AutoConfig
model_name = "bert-base-uncased"
base_model = AutoModel.from_pretrained(model_name)
config = AutoConfig.from_pretrained(model_name)
prefix_length = 10
model = ModelWithPrefixTuning(base_model, prefix_length)
# 只训练 Prefix 的参数
for name, param in model.named_parameters():
if "prefix" not in name:
param.requires_grad = False
# 计算可训练参数的数量
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"可训练参数数量: {trainable_params}")
注意: 上面的代码示例是一个简化的版本。在实际应用中,需要根据具体的模型架构进行调整。 例如,可能需要将 prefix 插入到模型的多个层中,或者使用更复杂的 prefix 结构。
3.3 LoRA (Low-Rank Adaptation)
LoRA 通过学习低秩矩阵来近似预训练模型的权重更新。 具体来说,对于预训练模型的每个权重矩阵,LoRA 添加一个并行的低秩矩阵分解:
W = W_0 + BA
其中:
W_0是预训练模型的原始权重矩阵。B和A是两个低秩矩阵,它们的秩远小于W_0的秩。BA近似于W的更新量。
原理: LoRA 通过学习低秩矩阵,可以有效地减少需要训练的参数量,同时保持模型的性能。
优点:
- 可以显著减少需要训练的参数量。
- 易于实现和集成。
- 可以灵活地调整低秩矩阵的秩,以控制微调的参数量。
缺点:
- 需要仔细选择需要应用 LoRA 的权重矩阵。
- 低秩矩阵的秩需要仔细调整,以避免影响模型性能。
代码示例 (使用 PyTorch):
import torch
import torch.nn as nn
class LoRA(nn.Module):
def __init__(self, original_weight, rank):
super().__init__()
self.original_weight = original_weight
self.rank = rank
self.A = nn.Parameter(torch.randn(original_weight.shape[1], rank) / 10)
self.B = nn.Parameter(torch.randn(rank, original_weight.shape[0]) / 10)
def forward(self, x):
return x + torch.matmul(x, torch.matmul(self.A, self.B).transpose(0, 1))
class ModelWithLoRA(nn.Module):
def __init__(self, base_model, lora_rank):
super().__init__()
self.base_model = base_model
self.lora_modules = []
for name, module in self.base_model.named_modules():
if isinstance(module, nn.Linear): # Apply LoRA to Linear layers
lora = LoRA(module.weight.data, lora_rank)
self.lora_modules.append(lora)
module.weight = lora # Replace the original weight with the LoRA module
def forward(self, input_ids, attention_mask):
return self.base_model(input_ids, attention_mask=attention_mask)
# 示例用法
from transformers import AutoModel
model_name = "bert-base-uncased"
base_model = AutoModel.from_pretrained(model_name)
lora_rank = 8
model = ModelWithLoRA(base_model, lora_rank)
# 只训练 LoRA 的参数
for name, param in model.named_parameters():
if "lora" not in name:
param.requires_grad = False
# 计算可训练参数的数量
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"可训练参数数量: {trainable_params}")
注意: 上述代码片段只是一个示例,并非所有的 nn.Linear 层都适合应用 LoRA 。 通常,需要根据模型的具体架构和任务,选择合适的层应用 LoRA。 另外,LoRA 的实现方式可以有多种,例如,可以将 LoRA 应用于模型的注意力层,或者将 LoRA 应用于模型的嵌入层。
3.4 BitFit
BitFit 是一种非常简单有效的 PEFT 方法。 它只微调模型中的 bias 项。
原理: BitFit 认为,bias 项包含了模型中与特定任务相关的关键信息。 通过只微调 bias 项,可以有效地适应下游任务,同时保持预训练模型的通用知识。
优点:
- 非常简单易于实现。
- 可以显著减少需要训练的参数量。
- 通常可以获得与全参数微调相当的性能。
缺点:
- 可能不适用于所有任务。
- 对于某些任务,可能需要与其他 PEFT 方法结合使用。
代码示例 (使用 PyTorch):
import torch
import torch.nn as nn
# 示例用法
from transformers import AutoModel
model_name = "bert-base-uncased"
model = AutoModel.from_pretrained(model_name)
# 只训练 bias 的参数
for name, param in model.named_parameters():
if "bias" not in name:
param.requires_grad = False
# 计算可训练参数的数量
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"可训练参数数量: {trainable_params}")
3.5 (IA)^3
(IA)^3 (Infused Adapter by Inhibiting and Amplifying Inner Activations) 是一种选择性微调技术。 它在每一层中引入三个缩放向量 (scaling vectors),分别用于缩放 query, key 和 value 矩阵的激活值。
原理: (IA)^3 通过学习缩放向量,可以选择性地放大或抑制模型内部的激活值,从而使模型更好地适应下游任务。
优点:
- 可以有效地控制模型的行为。
- 可以在不同的任务之间共享预训练模型。
- 可以灵活地调整缩放向量的大小,以控制微调的参数量。
缺点:
- 实现起来比 Adapter 和 LoRA 稍微复杂一些。
- 需要仔细调整缩放向量的初始化方式,以避免影响模型性能。
这里不提供代码示例,因为实现较为复杂,可以直接使用现有的库,例如 Hugging Face 的 peft 库。
4. PEFT 技术的选择策略
选择合适的 PEFT 技术取决于具体的任务、模型和资源限制。 以下是一些建议:
- 对于资源非常有限的情况: BitFit 可能是最佳选择,因为它只需要微调 bias 项,参数量最少。
- 对于需要高度控制模型行为的情况: Prefix-Tuning 或 (IA)^3 可能是更好的选择,因为它们可以直接控制模型的内部状态。
- 对于需要平衡性能和效率的情况: Adapter 或 LoRA 可能是不错的选择,它们可以在减少参数量的同时,保持模型的性能。
此外,还可以尝试将不同的 PEFT 技术结合起来使用,例如,可以将 LoRA 与 Adapter 结合使用,以进一步减少参数量。
下表总结了各种 PEFT 技术的特点:
| PEFT 技术 | 参数量 | 实现难度 | 性能 | 适用场景 |
|---|---|---|---|---|
| Adapter | 中等 | 简单 | 良好 | 适用于需要快速适应不同任务,并且对推理速度要求不高的场景。 |
| Prefix-Tuning | 中等 | 中等 | 良好 | 适用于需要高度控制模型行为的场景,例如生成任务。 |
| LoRA | 低 | 简单 | 良好 | 适用于需要显著减少参数量,并且对模型性能要求较高的场景。 |
| BitFit | 非常低 | 非常简单 | 一般 | 适用于资源非常有限,并且对模型性能要求不高的场景。 |
| (IA)^3 | 中等 | 复杂 | 良好 | 适用于需要选择性地放大或抑制模型内部的激活值,并且对模型性能要求较高的场景。 |
5. 使用 Hugging Face peft 库简化 PEFT 流程
Hugging Face 的 peft 库提供了一个统一的接口,可以方便地使用各种 PEFT 技术。 该库支持多种预训练模型,例如 BERT、GPT、T5 等。
安装 peft 库:
pip install peft
使用 peft 库进行 LoRA 微调的示例代码:
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from peft import LoraConfig, get_peft_model
# 加载预训练模型和 tokenizer
model_name_or_path = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path, num_labels=2)
# 配置 LoRA
config = LoraConfig(
r=8, # LoRA rank
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="SEQ_CLS" # Sequence classification
)
# 使用 LoRA 包装模型
model = get_peft_model(model, config)
model.print_trainable_parameters()
# 训练模型 (使用 PyTorch Trainer 或其他训练框架)
# ...
# 保存模型
model.save_pretrained("lora_model")
# 加载模型
from peft import PeftModel
model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path, num_labels=2)
model = PeftModel.from_pretrained(model, "lora_model")
使用 peft 库可以极大地简化 PEFT 的流程,减少代码量,并提高开发效率。
6. 企业如何利用 PEFT 降低 GPU 成本
企业可以从以下几个方面利用 PEFT 技术来降低 GPU 成本:
- 减少 GPU 资源需求: PEFT 技术可以显著减少需要训练的参数量,从而降低 GPU 内存需求和计算量。
- 缩短训练时间: 由于参数量减少,训练速度更快,从而缩短了 GPU 的使用时间。
- 降低存储成本: PEFT 技术只需要存储少量参数,从而降低了存储成本。
- 更高效地利用 GPU 集群: 由于每个任务所需的 GPU 资源减少,企业可以在相同的 GPU 集群上运行更多的任务。
- democratize AI: 使得即使没有大量计算资源的企业也能负担得起微调大型模型,从而 democratize AI。
7. PEFT 的局限性
尽管 PEFT 技术有很多优点,但也存在一些局限性:
- 并非所有任务都适用: 对于某些任务,PEFT 技术可能无法达到与全参数微调相当的性能。
- 需要仔细调整超参数: PEFT 技术的性能高度依赖于超参数的选择,例如 Adapter 的大小、LoRA 的秩等。
- 可能需要更多的实验: 为了找到最佳的 PEFT 技术和超参数,可能需要进行更多的实验。
- 推理速度的影响: 某些 PEFT 方法,例如 Adapter 和 Prefix-Tuning,可能会引入额外的推理延迟。
8. 未来发展趋势
PEFT 技术是一个快速发展的领域,未来可能会出现更多更有效的 PEFT 方法。 一些可能的发展趋势包括:
- 自动化超参数优化: 自动搜索最佳的 PEFT 技术和超参数。
- 更高效的 PEFT 方法: 开发更高效的 PEFT 方法,例如,通过知识蒸馏来进一步压缩模型。
- 更广泛的应用: 将 PEFT 技术应用于更多的任务和模型。
- 与其他技术的结合: 将 PEFT 技术与其他技术结合使用,例如,与量化、剪枝等技术结合使用,以进一步降低计算成本。
结论:经济高效的微调方案
PEFT 技术为企业提供了一种经济高效的微调方案,可以显著降低 GPU 成本,加速模型训练,并 democratize AI。 通过选择合适的 PEFT 技术,并仔细调整超参数,企业可以充分利用 PEFT 技术的优势,提升模型性能,并降低运营成本。 随着 PEFT 技术的不断发展,相信未来会有更多更有效的 PEFT 方法出现,为深度学习的应用带来更大的便利。