目录
一、PEFT的关键概念和方法
部分参数微调
概念
方法
优势
适配器(Adapters)
方法
优势
低秩分解(Low-rank Factorization)
方法
优势
差分微调(Delta Tuning)
方法
优势
多任务学习(Multi-task Learning)
方法
优势
二、低秩矩阵分解技术
低秩分解的原理
常见的低秩分解方法
奇异值分解(Singular Value Decomposition, SVD)
主成分分析(Principal Component Analysis, PCA)
非负矩阵分解(Non-negative Matrix Factorization, NMF)
低秩分解的应用
例子
三、部分参数微调
部分参数微调的原理
常见的部分参数微调方法
顶层微调(Top-layer Tuning)
中间层微调(Intermediate-layer Tuning)
瓶颈层微调(Bottleneck-layer Tuning)
层归一化参数微调(Layer Normalization Parameter Tuning)
特定参数组微调(Specific Parameter Group Tuning)
代码实例
四、适配器
适配器的原理
适配器的结构
降维层(Down-projection layer)
非线性激活函数
升维层(Up-projection layer)
残差连接
适配器的插入位置
代码实例
五、差分微调(Delta Tuning)
差分微调的原理
代码实例
六、多任务学习(Multi-task Learning)
多任务学习的原理
多任务学习的方法
硬共享(Hard Parameter Sharing)
软共享(Soft Parameter Sharing)
代码实例
PEFT(Parameter-Efficient Fine-Tuning)是一种在大规模预训练模型(如Transformer模型)上进行高效微调的方法。这种方法的主要目标是通过优化较少的参数来实现模型的高效微调,从而降低计算成本和存储需求,同时保持或接近原始模型的性能。PEFT 在实际应用中非常重要,特别是在资源受限的环境中。
一、PEFT的关键概念和方法
部分参数微调
概念
部分参数微调是一种只调整模型中特定参数或层的方法,仅微调模型的一部分参数而不是整个模型,例如某些特定的层、子网络或参数组。这种方法选择性地微调一些关键部分,这可以大大减少需要优化的参数数量,从而减少计算负担和内存使用。
方法
冻结大部分层:只微调最后几层或特定的中间层。
选择性解冻:在训练过程中,逐步解冻更多的层。
层归一化:只微调归一化层的参数,如BatchNorm层或LayerNorm层。
优势
高效性:减少需要优化的参数数量,节省计算资源。
稳定性:通过限制参数更新的范围,减少过拟合风险。
适配器(Adapters)
在模型的特定层中插入小的适配器模块,这些模块通常比原始层小得多,只需微调适配器的参数,适配器的参数是独立优化的。这种方法可以在保持原始模型架构的前提下实现高效微调。
方法
在Transformer层中插入适配器:通常插入在每个Transformer层的前馈网络部分。
参数共享:在多任务学习中,不同任务的适配器可以共享部分参数。
优势
灵活性:适配器模块可以插入到不同层中,适应不同任务的需求。
节省资源:大大减少需要微调的参数量。
低秩分解(Low-rank Factorization)
将模型参数矩阵分解成两个较小的矩阵进行优化。这种方法可以减少参数量,同时保持模型的表示能力。
方法
矩阵分解:将一个大矩阵 W 分解为两个小矩阵 A 和 B,即 W≈A×B。
训练过程:只微调小矩阵 A 和 B,而不更新整个大矩阵。
优势
减少参数量:有效降低模型的参数规模。
保持性能:在很多情况下,可以保持模型性能不变或略微下降。
差分微调(Delta Tuning)
只微调与原始模型参数的差值部分,而不是整个参数集。这种方法可以在节省计算资源的同时,实现有效的参数更新。这种方法通过优化参数的增量来实现模型调整。
方法
参数初始化:从预训练模型加载参数。
增量更新:仅优化参数的增量部分 Δθ,即 Δθ′=θ+Δθ。
优势
节省内存:只存储和更新参数的增量部分。
稳定性:原始模型参数作为基础,有助于保持模型性能。
多任务学习(Multi-task Learning)
多任务学习是一种通过共享参数在多个任务之间进行微调的方法。这种方法利用多个任务的共同信息,提高模型的泛化能力。
方法
共享层:在多个任务之间共享部分模型层,减少总参数量。
任务特定层:每个任务拥有一些特定的参数层,用于处理任务特有的信息。
交替训练:在不同任务的数据上交替进行训练。
优势
参数共享:通过共享参数,显著减少总参数量。
提高泛化能力:利用多个任务的共同信息,提高模型的泛化性能。
二、低秩矩阵分解技术
低秩分解(Low-rank Factorization)是一种在机器学习和信号处理领域中广泛应用的技术,主要用于降维、压缩和优化模型参数。通过将一个高维矩阵分解成两个或多个低维矩阵,低秩分解可以有效减少参数数量,同时保持原始矩阵的大部分信息和结构特征。
低秩分解的原理
低秩分解基于矩阵的秩(rank),即矩阵中线性独立行或列的最大数目。低秩分解通过将一个高秩矩阵近似为两个或多个低秩矩阵的乘积,从而降低参数维度。常见的低秩分解方法包括SVD(奇异值分解)、PCA(主成分分析)、NMF(非负矩阵分解)等。
给定一个矩阵,其低秩分解可以表示为:
其中,和,k 是低秩近似的秩,通常 k 远小于 m 和 n。
常见的低秩分解方法
奇异值分解(Singular Value Decomposition, SVD)
原理:SVD将矩阵分解为三个矩阵的乘积,即 ,其中 U 和 V 是正交矩阵, ΣΣ 是对角矩阵,其对角线上的元素为奇异值。
低秩近似:选择前 k 个最大的奇异值及其对应的奇异向量,得到矩阵的低秩近似
主成分分析(Principal Component Analysis, PCA)
原理:PCA通过对数据进行协方差矩阵的特征值分解,找到数据的主成分,即方差最大的方向。
低秩近似:选择前 k 个主成分构建新的低维空间,从而实现降维。
非负矩阵分解(Non-negative Matrix Factorization, NMF)
原理:NMF将一个非负矩阵分解为两个非负矩阵的乘积,即 ,其中 A 和 B 均为非负矩阵。
低秩近似:通过优化目标函数(如平方误差)找到最优的非负矩阵 A 和 B。
低秩分解的应用
降维:通过低秩分解将高维数据映射到低维空间,从而减少计算复杂度,提高模型效率。
压缩:在深度学习中,通过低秩分解压缩权重矩阵,减少模型参数量和存储需求。
去噪:低秩分解能够有效去除数据中的噪声,提高数据的质量和模型的鲁棒性。
推荐系统:在协同过滤中,低秩分解用于分解用户-物品评分矩阵,预测用户对未评分物品的偏好。
例子
假设有一个权重矩阵,我们希望将其分解为两个低秩矩阵 A 和 B,其中和。通过低秩分解,我们将原始矩阵的参数数量从减少到,显著降低了参数量和计算成本。
三、部分参数微调
部分参数微调(Partial Parameter Tuning)是一种高效优化方法,通过选择性地微调模型中的一部分参数来实现模型的适应性调整。这个方法适用于大规模预训练模型(如BERT、GPT等),能够在节省计算资源的同时保持模型性能。可以大幅减少需要优化的参数数量,降低计算成本和训练时间。通过限制参数更新的范围,降低模型过拟合的风险。在计算资源有限的情况下,快速适应新任务或新数据。
部分参数微调的原理
部分参数微调基于以下原则:
冻结大部分参数:保持模型的大部分参数不变,仅微调特定层或参数组。
选择性解冻:逐步解冻和微调更多层或参数,以逐步适应新的任务或数据。
常见的部分参数微调方法
顶层微调(Top-layer Tuning)
方法:只微调模型的最后几层或输出层。这种方法通常用于下游分类或回归任务,因为顶层参数直接影响模型的最终输出。
优势:大幅减少需要优化的参数数量,同时保留预训练模型提取的底层特征。
中间层微调(Intermediate-layer Tuning)
方法:选择模型的中间层进行微调。这种方法适用于需要调整模型内部特征表示的任务。
优势:灵活性较高,可以根据具体任务选择最相关的层进行微调。
瓶颈层微调(Bottleneck-layer Tuning)
方法:微调网络中的瓶颈层,即那些对信息流具有瓶颈效应的层(如Transformer中的注意力层)。
优势:通过微调瓶颈层,可以有效调整模型的表示能力,适应不同的任务需求。
层归一化参数微调(Layer Normalization Parameter Tuning)
方法:只微调层归一化(LayerNorm)或批归一化(BatchNorm)层的参数。
优势:归一化层参数较少,但对模型的稳定性和收敛速度有重要影响,因此微调这些参数可以带来显著的性能提升。
特定参数组微调(Specific Parameter Group Tuning)
方法:根据任务需求,选择性地微调特定参数组,如词嵌入层参数或特定注意力头的参数。
优势:精细控制微调过程,优化最相关的参数,节省资源。
代码实例
冻结大部分层:只微调BERT模型的最后一层Transformer层和输出层。
from transformers import BertModel, BertForSequenceClassification
import torchmodel = BertForSequenceClassification.from_pretrained('bert-base-uncased')# 冻结所有层
for param in model.bert.parameters():param.requires_grad = False# 只微调最后一层和分类头
for param in model.bert.encoder.layer[-1].parameters():param.requires_grad = True
for param in model.classifier.parameters():param.requires_grad = Trueoptimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4)
选择性解冻:逐步解冻更多的Transformer层进行微调。
# 逐步解冻更多层
for i in range(-1, -4, -1): # 解冻最后三层for param in model.bert.encoder.layer[i].parameters():param.requires_grad = True
四、适配器
适配器(Adapters)是部分参数微调的一种具体实现方法,通过在预训练模型的特定层中插入小型的适配器模块来实现模型微调。适配器的设计初衷是为了在保持预训练模型的大部分参数不变的情况下,实现对新任务的适应性调整。
适配器的原理
适配器的基本思想是将适配器模块插入到预训练模型的各个层中,这些模块通常包含少量参数,并且在微调过程中只更新这些参数。这样做的好处是减少了需要优化的参数量,同时利用了预训练模型中已学习到的丰富特征。
适配器的结构
降维层(Down-projection layer)
将输入特征降维到一个较小的空间。通常是一个线性变换,例如全连接层。,其中, x 是输入特征,和 是降维层的权重和偏置。
非线性激活函数
在降维层之后,应用非线性激活函数(如ReLU,)来引入非线性特性。
升维层(Up-projection layer)
将降维后的特征升维回原始空间。也是一个线性变换。
其中,和是升维层的权重和偏置。
残差连接
将升维后的特征与原始输入特征相加,形成残差连接。这可以帮助模型保持原有的特征表示,同时引入适配器模块的调整。
适配器模块的整体结构如下:
适配器的插入位置
适配器模块可以插入到预训练模型的不同位置,常见的插入位置包括:
Transformer层内部:在Transformer层的多头注意力子层和前馈神经网络子层之间插入适配器模块。
每个Transformer层之后:在每个Transformer层之后插入适配器模块。
特定层中:根据任务需求,在特定的层中插入适配器模块。
代码实例
import torch
import torch.nn as nn
from transformers import BertModel, BertConfigclass Adapter(nn.Module):def __init__(self, input_dim, bottleneck_dim):super(Adapter, self).__init__()self.down_proj = nn.Linear(input_dim, bottleneck_dim)self.up_proj = nn.Linear(bottleneck_dim, input_dim)self.activation = nn.ReLU()def forward(self, x):z = self.down_proj(x)z = self.activation(z)z = self.up_proj(z)return x + zclass BertWithAdapters(nn.Module):def __init__(self, model_name, adapter_dim):super(BertWithAdapters, self).__init__()self.bert = BertModel.from_pretrained(model_name)self.adapters = nn.ModuleList([Adapter(self.bert.config.hidden_size, adapter_dim) for _ in range(self.bert.config.num_hidden_layers)])def forward(self, input_ids, attention_mask=None, token_type_ids=None):outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)sequence_output = outputs[0]for i, adapter in enumerate(self.adapters):sequence_output = adapter(sequence_output)return sequence_output# 使用适配器微调BERT模型
model = BertWithAdapters('bert-base-uncased', adapter_dim=64)
optimizer = torch.optim.Adam(model.adapters.parameters(), lr=1e-4)
五、差分微调(Delta Tuning)
差分微调(Delta Tuning)是一种高效的模型微调方法,通过仅优化与预训练模型参数的差值部分(增量),而不是整个参数集,从而降低计算和存储成本。这种方法特别适用于大规模预训练模型,如BERT、GPT等,在实际应用中能有效地减少微调时的资源消耗。
差分微调的原理
差分微调的基本思想是将模型的参数表示为预训练参数和微调增量的组合。具体来说,对于预训练模型的参数 θ,我们在微调过程中引入一个增量参数 Δθ,使得微调后的参数 θ′ 可以表示为:
在训练过程中,我们只优化增量参数 Δθ,保持预训练参数 θ 不变。
代码实例
加载预训练模型:
from transformers import BertModel, BertConfig# 加载预训练的BERT模型
model = BertModel.from_pretrained('bert-base-uncased')
pretrained_params = {name: param.clone() for name, param in model.named_parameters()}
定义增量参数
创建增量参数 Δθ,并将其初始化为零。
import torch# 定义增量参数
delta_params = {name: torch.zeros_like(param) for name, param in pretrained_params.items()}
微调模型
在训练过程中,只更新增量参数 Δθ。
optimizer = torch.optim.Adam(delta_params.values(), lr=1e-4)for epoch in range(num_epochs):for batch in data_loader:# 前向传播outputs = model(input_ids=batch['input_ids'], attention_mask=batch['attention_mask'])# 计算损失loss = loss_function(outputs, batch['labels'])# 反向传播optimizer.zero_grad()loss.backward()# 更新增量参数optimizer.step()# 更新模型参数for name, param in model.named_parameters():param.data = pretrained_params[name] + delta_params[name]
完整的demo:
import torch
from transformers import BertModel, BertTokenizerclass DeltaBertModel(nn.Module):def __init__(self, model_name):super(DeltaBertModel, self).__init__()self.bert = BertModel.from_pretrained(model_name)self.delta_params = nn.ParameterDict({name: nn.Parameter(torch.zeros_like(param))for name, param in self.bert.named_parameters()})def forward(self, input_ids, attention_mask=None, token_type_ids=None):outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)return outputsdef update_parameters(self):for name, param in self.bert.named_parameters():param.data += self.delta_params[name].data# 初始化模型和优化器
model_name = 'bert-base-uncased'
model = DeltaBertModel(model_name)
optimizer = torch.optim.Adam(model.delta_params.parameters(), lr=1e-4)# 模拟训练循环
for epoch in range(num_epochs):for batch in data_loader:input_ids = batch['input_ids']attention_mask = batch['attention_mask']labels = batch['labels']# 前向传播outputs = model(input_ids, attention_mask=attention_mask)logits = outputs[0]# 计算损失loss = loss_function(logits, labels)# 反向传播optimizer.zero_grad()loss.backward()optimizer.step()# 更新模型参数model.update_parameters()
六、多任务学习(Multi-task Learning)
多任务学习(Multi-task Learning, MTL)是一种机器学习方法,通过同时学习多个相关任务来提高模型的泛化能力和效率。与单任务学习不同,多任务学习旨在通过共享不同任务之间的信息,利用它们的共同特性,提升整体模型的性能。多任务学习在自然语言处理、计算机视觉、推荐系统等领域有广泛的应用。
多任务学习的原理
多任务学习的基本思想是将多个相关任务放在同一个模型中进行训练,这些任务共享部分模型参数,从而实现知识的共享和互补。多任务学习的主要目标是通过共享不同任务之间的信息来提高模型的泛化能力,减少过拟合风险。
多任务学习的方法
多任务学习的实现可以通过多种方法,主要包括硬共享(hard parameter sharing)和软共享(soft parameter sharing)两种。
硬共享(Hard Parameter Sharing)
硬共享是多任务学习中最常见的方法,多个任务共享模型的部分层或参数。在这种方法中,底层网络的参数在所有任务之间共享,而任务特定的参数只在各自的任务上进行微调。
import torch
import torch.nn as nnclass SharedModel(nn.Module):def __init__(self):super(SharedModel, self).__init__()self.shared_layers = nn.Sequential(nn.Linear(768, 256),nn.ReLU(),nn.Linear(256, 128),nn.ReLU())self.task1_head = nn.Linear(128, 10) # 任务1的输出层self.task2_head = nn.Linear(128, 5) # 任务2的输出层def forward(self, x, task):x = self.shared_layers(x)if task == 'task1':return self.task1_head(x)elif task == 'task2':return self.task2_head(x)# 创建模型和优化器
model = SharedModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)# 训练循环
for epoch in range(num_epochs):for batch in data_loader:inputs, labels, task = batch['inputs'], batch['labels'], batch['task']optimizer.zero_grad()outputs = model(inputs, task)loss = loss_function(outputs, labels)loss.backward()optimizer.step()
软共享(Soft Parameter Sharing)
软共享通过为每个任务定义独立的模型参数,同时在训练过程中通过正则化项使得不同任务的参数尽可能相似。这种方法保留了任务特定的特征,同时实现了信息共享。
class SoftSharedModel(nn.Module):def __init__(self):super(SoftSharedModel, self).__init__()self.task1_layers = nn.Sequential(nn.Linear(768, 256),nn.ReLU(),nn.Linear(256, 128),nn.ReLU())self.task2_layers = nn.Sequential(nn.Linear(768, 256),nn.ReLU(),nn.Linear(256, 128),nn.ReLU())self.shared_layers = nn.Sequential(nn.Linear(128, 64),nn.ReLU(),nn.Linear(64, 32),nn.ReLU())self.task1_head = nn.Linear(32, 10)self.task2_head = nn.Linear(32, 5)def forward(self, x, task):if task == 'task1':x = self.task1_layers(x)elif task == 'task2':x = self.task2_layers(x)x = self.shared_layers(x)if task == 'task1':return self.task1_head(x)elif task == 'task2':return self.task2_head(x)# 创建模型和优化器
model = SoftSharedModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)# 训练循环
for epoch in range(num_epochs):for batch in data_loader:inputs, labels, task = batch['inputs'], batch['labels'], batch['task']optimizer.zero_grad()outputs = model(inputs, task)loss = loss_function(outputs, labels)loss.backward()optimizer.step()
代码实例
from transformers import BertModel, BertTokenizerclass MultiTaskBertModel(nn.Module):def __init__(self, model_name):super(MultiTaskBertModel, self).__init__()self.bert = BertModel.from_pretrained(model_name)self.shared_layers = nn.Linear(768, 128)self.task1_head = nn.Linear(128, 10) # 任务1:文本分类self.task2_head = nn.Linear(128, 5) # 任务2:情感分析def forward(self, input_ids, attention_mask=None, token_type_ids=None, task=None):outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)pooled_output = outputs[1] # 使用BERT的[CLS]标记的输出shared_output = self.shared_layers(pooled_output)if task == 'task1':return self.task1_head(shared_output)elif task == 'task2':return self.task2_head(shared_output)# 创建模型和优化器
model_name = 'bert-base-uncased'
model = MultiTaskBertModel(model_name)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)# 训练循环
for epoch in range(num_epochs):for batch in data_loader:input_ids = batch['input_ids']attention_mask = batch['attention_mask']labels = batch['labels']task = batch['task']optimizer.zero_grad()outputs = model(input_ids, attention_mask=attention_mask, task=task)loss = loss_function(outputs, labels)loss.backward()optimizer.step()