使用LoRA进行高效微调:基本原理

Using LoRA for efficient fine-tuning: Fundamental principles — ROCm Blogs (amd.com)

[2106.09685] LoRA: Low-Rank Adaptation of Large Language Models (arxiv.org)

Parametrizations Tutorial — PyTorch Tutorials 2.3.0+cu121 documentation

大型语言模型(LLMs)的低秩适应(Low-Rank Adaptation,简称LoRA)用于解决微调大型语言模型时面临的挑战。像GPT和Llama这样的模型,拥有数十亿个参数,通常对于特定任务或领域的微调来说成本过高。LoRA保留了预训练模型的权重,并在每个模型块内部加入了可训练的层。这导致需要微调的参数数量显著减少,并大幅降低了GPU内存需求。LoRA的关键优势在于,它大幅减少了可训练参数的数量——有时高达10,000倍——从而显著降低了对GPU资源的需求。

为什么LoRA有效

当预训练的大型语言模型(LLMs)适应新任务时,它们具有较低的“内在维度”,这意味着数据可以在保留大部分关键信息或结构的同时,通过较低维度的空间进行有效表示或近似。我们可以通过将适应任务的新权重矩阵分解为较低维度(较小)的矩阵来实现这一点,而不会丢失太多重要信息。我们通过低秩近似来实现这一点。

矩阵的秩是一个值,它让你了解矩阵的复杂性。矩阵的低秩近似旨在尽可能接近地逼近原始矩阵,但具有较低的秩。较低秩的矩阵降低了计算复杂度,从而提高了矩阵乘法的效率。低秩分解是指通过导出A的低秩近似来有效逼近矩阵A的过程。奇异值分解(SVD)是低秩分解的一种常见方法。

假设W表示给定神经网络层中的权重矩阵,假设ΔW是经过完整微调后W的权重更新。然后,我们可以将权重更新矩阵ΔW分解为两个较小的矩阵:ΔW = WA*WB,其中WA是A×r维矩阵,WB是r×B维矩阵。在这里,我们保持原始权重W不变,只训练新的矩阵WA和WB。这总结了LoRA方法,如下图所示。

LoRA的好处

  1. 降低资源消耗:微调深度学习模型通常需要大量的计算资源,这可能既昂贵又耗时。LoRA在保持高性能的同时,降低了对资源的需求。

  2. 更快的迭代:LoRA能够实现快速迭代,使得尝试不同的微调任务和快速适应模型变得更加容易。

  3. 改进的迁移学习:LoRA增强了迁移学习的效果,因为带有LoRA适配器的模型可以用更少的数据进行微调。这在标签数据稀缺的情况下尤其有价值。

  4. 广泛的应用性:LoRA具有通用性,可以应用于各种领域,包括自然语言处理、计算机视觉和语音识别等。

  5. 更低的碳足迹:通过降低计算需求,LoRA有助于实现更绿色、更可持续的深度学习方法。

使用LoRA技术训练神经网络

我们的目标是使用LoRA技术训练一个神经网络,用于MNIST手写数字数据库的分类任务,并随后对该网络进行微调,以提升其在初始表现不佳的类别上的性能。

硬件要求

  • AMD Instinct GPU

软件环境

  • ROCm:ROCm是针对AMD GPU优化的开源机器学习平台。
  • PyTorch:PyTorch是广泛使用的深度学习框架,支持动态计算图。
  • tqdm:Python库,用于显示进度条,方便观察训练过程。

以下是一个简化的步骤指南,说明如何使用LoRA技术训练神经网络(注意:具体代码细节可能需要根据您的环境和PyTorch版本进行调整):

  1. 准备数据集
    首先,您需要准备MNIST数据集。PyTorch提供了torchvision.datasets.MNIST来方便加载这个数据集。

  2. 定义模型
    选择一个预训练的神经网络模型,例如ResNet、Transformer等。为了简单起见,您可以选择一个轻量级的卷积神经网络(CNN)。

  3. 添加LoRA层
    在模型的每个权重矩阵上添加LoRA层。这通常涉及在模型内部插入额外的可训练权重矩阵(如WAWB),它们将用于生成对原始权重矩阵的低秩更新。

  4. 初始化模型
    加载预训练的模型权重,并将LoRA层的权重初始化为零或小的随机值。

  5. 设置训练循环
    使用PyTorch的数据加载器(DataLoader)和优化器(如SGD或Adam)来设置训练循环。确保在训练循环中同时更新原始模型的权重和LoRA层的权重。

  6. 训练模型
    运行训练循环,通过反向传播和优化器步骤来更新权重。由于LoRA层的存在,大多数权重更新将仅应用于LoRA层,而不是整个模型。

  7. 评估模型
    在验证集上评估模型的性能,并根据需要调整超参数或继续训练。

  8. 微调模型
    如果模型在特定类别上表现不佳,可以使用LoRA技术仅对该类别进行微调。这通常涉及重新训练LoRA层(同时保持原始模型权重不变),使用仅针对该类别的数据。

  9. 测试模型
    在测试集上测试微调后的模型,以评估其性能改进。

开始

1. 首先,我们需要导入一些必要的包。

import torch
import torchvision.datasets as datasets
import torchvision.transforms as transforms
import torch.nn as nn
from tqdm import tqdm

2. 设定随机数生成的种子,以确保模型的行为在每次运行时都是确定的。

# 设置随机种子以保证实验的可复现性
_ = torch.manual_seed(0)

我们通常不会将变量名 _ 作为赋值语句的结果,除非我们故意忽略该值。在这里,它仅用于表示我们不关心 torch.manual_seed(0) 的返回值,并使其确定性。

在训练神经网络时,我们通常希望结果是可重复的,这意味着如果我们用相同的初始参数和相同的训练数据重新运行模型,我们应该得到相同的结果。通过设置随机数生成的种子(使用 torch.manual_seed(0)),我们可以确保PyTorch在生成随机数(例如,在初始化权重或选择随机数据批次时)时使用相同的序列,从而使模型训练具有确定性。这在调试和比较不同模型或超参数时特别有用。

3. 加载数据集。

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])# 加载MNIST数据集
mnist_trainset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
# 为训练创建数据加载器
train_loader = torch.utils.data.DataLoader(mnist_trainset, batch_size=10, shuffle=True)# 加载MNIST测试集
mnist_testset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(mnist_testset, batch_size=10, shuffle=True)# 定义设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

4. 创建用于分类数字的神经网络(我们使用了更复杂的代码以更好地展示LoRA)。

# 创建一个过度昂贵的神经网络来分类MNIST数字
# 不关心效率
class RichBoyNet(nn.Module):def __init__(self, hidden_size_1=1000, hidden_size_2=2000):super(RichBoyNet,self).__init__()self.linear1 = nn.Linear(28*28, hidden_size_1)self.linear2 = nn.Linear(hidden_size_1, hidden_size_2)self.linear3 = nn.Linear(hidden_size_2, 10)self.relu = nn.ReLU()def forward(self, img):x = img.view(-1, 28*28)x = self.relu(self.linear1(x))x = self.relu(self.linear2(x))x = self.linear3(x)return xnet = RichBoyNet().to(device)
这段代码首先定义了一个数据预处理流程,用于将MNIST数据集中的图像转换为张量并进行归一化。然后,它加载了MNIST训练集和测试集,并为这两个数据集创建了数据加载器。接下来,它定义了一个设备,用于确定是在GPU上还是在CPU上运行模型。最后,它定义了一个名为RichBoyNet的神经网络类,该类包含三个全连接层和一个ReLU激活函数。最后,它创建了这个网络的实例,并将其移动到指定的设备上。

5. 对网络进行一轮训练,以模拟在数据上的完整预训练过程。在AMD Instinct GPU上,此过程只需数秒。

这段代码定义了一个名为train的函数,用于训练一个神经网络模型(在这里称为net)一个或多个epoch(周期)。在每个epoch中,该函数会遍历训练数据集(由train_loader提供),并更新模型的权重以最小化预测输出和实际标签之间的交叉熵损失。
# 定义一个函数来训练网络  
def train(train_loader, net, epochs=5, total_iterations_limit=None):  # 使用交叉熵损失函数  cross_el = nn.CrossEntropyLoss()  # 使用Adam优化器来更新网络参数,学习率设置为0.001  optimizer = torch.optim.Adam(net.parameters(), lr=0.001)  total_iterations = 0  # 初始化总迭代次数为0  # 对于指定的epochs数量,进行循环  for epoch in range(epochs):  net.train()  # 设置网络为训练模式  loss_sum = 0  # 初始化损失总和为0  num_iterations = 0  # 初始化迭代次数为0  # 使用tqdm库包装train_loader,使其具有进度条功能,并显示当前epoch信息  data_iterator = tqdm(train_loader, desc=f'Epoch {epoch+1}')  # 如果指定了总的迭代次数限制,则更新tqdm的total值  if total_iterations_limit is not None:  data_iterator.total = total_iterations_limit  # 遍历训练数据  for data in data_iterator:  num_iterations += 1  # 迭代次数加1  total_iterations += 1  # 总迭代次数加1  x, y = data  # 解包数据(输入x和标签y)  x = x.to(device)  # 将输入数据移动到指定的设备(如GPU)  y = y.to(device)  # 将标签数据移动到指定的设备  optimizer.zero_grad()  # 清零优化器的梯度  # 将输入数据展平(假设每个输入图像是28x28的),然后传递给网络  output = net(x.view(-1, 28*28))  # 计算预测输出和实际标签之间的交叉熵损失  loss = cross_el(output, y)  loss_sum += loss.item()  # 累加损失值  avg_loss = loss_sum / num_iterations  # 计算平均损失  # 更新tqdm的进度条,显示当前平均损失  data_iterator.set_postfix(loss=avg_loss)  # 执行反向传播以计算梯度  loss.backward()  # 使用优化器更新网络参数  optimizer.step()  # 如果指定了总的迭代次数限制,并且已经达到该限制,则退出训练  if total_iterations_limit is not None and total_iterations >= total_iterations_limit:  return  # 调用train函数,只训练一个epoch  
train(train_loader, net, epochs=1)

在这段代码中,device应该是一个预定义的变量,表示要使用的设备(CPU或GPU)。在实际使用中,你需要先定义device,例如device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),然后才能将数据移动到该设备上。此外,tqdm是一个用于显示进度条的Python库,如果你没有安装它,需要先使用pip install tqdm进行安装。

定义一个函数train用于对指定的数据加载器train_loader和网络net进行训练,可以选择训练的周期数epochs,默认为5,以及总的迭代次数限制total_iterations_limit

在函数中,首先初始化交叉熵损失函数CrossEntropyLoss和Adam优化器。然后,循环进行每个周期的训练:

  • 设置网络为训练模式。
  • 初始化损失总和loss_sum和迭代次数num_iterations
  • 使用tqdm包装训练数据加载器,以便显示进度。
  • 迭代数据加载器中的每个数据样本,执行以下操作:
    • 更新迭代次数。
    • 将输入数据x和标签y转换为适合GPU的格式。
    • 清除优化器的梯度。
    • 通过前向传播计算网络的输出。
    • 计算损失。
    • 累加损失并计算平均损失。
    • 将平均损失显示在进度条上。
    • 执行反向传播。
    • 更新网络权重。
    • 如果达到总迭代次数限制,提前结束训练。

最后,调用train函数,传入训练数据加载器、网络对象,并指定训练周期为1。

[!提示] 保留原始权重的副本(克隆它们),以便在微调后查看原始权重是否被更改。

original_weights = {}  
for name, param in net.named_parameters():  original_weights[name] = param.clone().detach()

在微调神经网络之前,我们首先遍历网络中的每一个参数(权重和偏置),并将它们保存在一个名为original_weights的字典中。这样,我们就可以在微调过程之后,通过比较original_weights中的权重和微调后网络中的权重,来查看原始权重是否已经被更改。

param.clone().detach()确保了我们在保存权重时创建了一个与原始权重完全相同的独立副本,这样即使在微调过程中原始权重发生更改,这个副本也不会受到影响。其中,clone()方法用于复制参数,detach()方法用于断开计算图,确保这个副本不参与任何梯度计算。

微调

1. 选择一个数字进行微调。预训练的网络在数字9上的表现不佳,所以我们将对这个数字进行微调。

def test():  correct = 0  total = 0  wrong_counts = [0 for i in range(10)]  # 初始化一个长度为10的列表,用于记录每个数字的错误次数  with torch.no_grad():  # 禁用梯度计算,因为我们只进行前向传播来评估模型  for data in tqdm(test_loader, desc='Testing'):  # tqdm是一个用于显示进度的工具  x, y = data  x = x.to(device)  # 将输入数据移动到指定的设备(如GPU)  y = y.to(device)  # 将标签数据移动到指定的设备  output = net(x.view(-1, 784))  # 假设每个图像是28x28的,因此将其展平为784个特征  # 遍历每个输出和对应的标签  for idx, i in enumerate(output):  if torch.argmax(i) == y[idx]:  # 如果预测的最大概率索引与真实标签相同  correct += 1  # 正确预测数加1  else:  wrong_counts[y[idx]] += 1  # 将对应标签的错误次数加1  total += 1  # 总测试样本数加1  print(f'Accuracy: {round(correct/total, 3)}')  # 打印准确率  # 打印每个数字的错误次数  for i in range(len(wrong_counts)):  print(f'数字 {i} 的错误次数: {wrong_counts[i]}')  # 调用测试函数  
test()

这段代码首先定义了一个test函数,用于在测试集上评估模型的性能。它计算了模型预测正确的样本数量,并统计了每个数字被错误预测的次数。然后,它输出了模型的总准确率和每个数字的错误次数。最后,通过调用test()函数来执行测试。

定义一个测试函数test,用于评估网络在MNIST测试集上的性能。首先初始化正确预测的数量correct和总预测数量total,以及一个用于统计每个数字被错误识别次数的列表wrong_counts

在不计算梯度的上下文中(torch.no_grad()),遍历测试数据加载器test_loader,并使用进度条显示测试进度。对于每个数据样本:

  • 将输入数据x和标签y转换为适合GPU的格式。
  • 通过前向传播计算网络的输出。
  • 遍历输出中每个预测结果,如果预测的类别与真实标签相同,则增加正确预测的数量;如果不同,则增加该数字的错误计数。
  • 更新总预测数量。

测试完成后,打印出整体的准确率,并遍历wrong_counts列表,打印出每个数字的错误计数。

最后,调用test函数执行测试过程。

2. 在引入LoRA矩阵之前,可视化原始网络中参数的数量。

# 打印网络权重矩阵的大小
# 保存总参数数量的计数
total_parameters_original = 0
for index, layer in enumerate([net.linear1, net.linear2, net.linear3]):total_parameters_original += layer.weight.nelement() + layer.bias.nelement()print(f'Layer {index+1}: W: {layer.weight.shape} + B: {layer.bias.shape}')
print(f'Total number of parameters: {total_parameters_original:,}')

上面的代码段通过遍历网络中的线性层(假设net.linear1net.linear2net.linear3是网络中的线性层),计算了每个层的权重(W)和偏置(B)的元素数量,并将它们累加到total_parameters_original变量中。最后,它打印出了每个层的权重和偏置的形状以及网络中的总参数数量。

3. 定义LoRA参数化。

LoRA(Low-Rank Adaptation)参数化是一种用于微调大型神经网络模型的技术,特别是那些已经经过预训练的模型。LoRA通过在原始权重矩阵上添加一个低秩更新项来实现参数的高效微调,而不是直接更新整个权重矩阵。

class LoRAParametrization(nn.Module):  def __init__(self, features_in, features_out, rank=1, alpha=1, device='cpu'):  super().__init__()  # 论文第4.1节:  # 我们使用随机高斯初始化A,并将B初始化为零,所以ΔW = BA在训练开始时为零  self.lora_A = nn.Parameter(torch.zeros((rank, features_out)).to(device))  # 初始化为零的低秩矩阵A  self.lora_B = nn.Parameter(torch.zeros((features_in, rank)).to(device))  # 初始化为零的低秩矩阵B  nn.init.normal_(self.lora_A, mean=0, std=1)  # 对A进行标准正态分布初始化  # 论文第4.1节:  # 我们对ΔWx进行α/r的缩放,其中α是一个与r无关的常数。  # 当使用Adam进行优化时,调整α大致相当于调整学习率,如果我们适当地缩放初始化。  # 因此,我们简单地将α设置为我们尝试的第一个r,并不调整它。  # 这种缩放有助于在改变r时减少重新调整超参数的需要。  self.scale = alpha / rank  # 缩放因子  self.enabled = True  # 标志位,表示是否启用LoRA更新  def forward(self, original_weights):  if self.enabled:  # 返回 W + (B*A)*scale  # 这里B*A是一个低秩矩阵,用于更新原始权重W  return original_weights + torch.matmul(self.lora_B, self.lora_A).view(original_weights.shape) * self.scale  else:  # 如果未启用LoRA,则直接返回原始权重  return original_weights

这个类定义了一个LoRA参数化模块,它可以在原始权重上添加一个低秩更新项。这个更新项由两个低秩矩阵lora_Alora_B的乘积以及一个缩放因子scale组成。在训练过程中,这个模块会优化这两个低秩矩阵,而不是整个权重矩阵,从而大大减少了需要更新的参数数量,提高了微调的效率和效果。

定义一个名为LoRAParametrization的类,它是一个神经网络模块。

在初始化函数__init__中:

  • 调用父类nn.Module的初始化函数。

  • 根据论文的4.1节,对lora_A使用随机高斯初始化,对lora_B使用零初始化,这样训练开始时∆W = BA为零。

  • lora_Alora_B被定义为神经网络的参数,分别初始化为大小为(rank, features_out)(features_in, rank)的零矩阵,并指定设备device

  • 使用nn.init.normal_函数对lora_A进行正态分布初始化,均值为0,标准差为1。

  • 根据论文的4.1节,∆Wxα/r的比例进行缩放,其中αr中的一个常数。

  • 当使用Adam优化器时,调整α大致上与调整学习率相同,如果我们适当地缩放初始化。

  • 因此,我们简单地将α设置为我们尝试的第一个r的值,并且不进行调整。

  • 这种缩放有助于减少我们在变化r时需要重新调整超参数的需求。

  • 计算缩放因子self.scale = alpha / rank

  • 设置一个标志self.enabledTrue,表示LoRA参数化被启用。

在前向传播函数forward中:

  • 如果self.enabledTrue,则计算W + (B*A)*scale,即将原始权重original_weights与其对应的lora_Blora_A的矩阵乘法结果进行缩放后相加。
  • 如果self.enabledFalse,则直接返回原始权重original_weights

4. 将参数化添加到我们的网络中。可以在PyTorch.org上了解更多关于PyTorch参数化的信息。

import torch.nn.utils.parametrize as parametrize  # 定义一个函数来将LoRA参数化应用到线性层的权重矩阵上  
def linear_layer_parameterization(layer, device, rank=1, lora_alpha=1):  # 仅对权重矩阵添加参数化,忽略偏置项  # 根据论文第4.2节:  # 我们的研究仅限于为下游任务调整注意力权重,并冻结MLP模块(因此在下游任务中它们不会被训练)  # [...]  # 我们将[...]和偏置项的实证研究留给未来的工作。  features_in, features_out = layer.weight.shape  return LoRAParametrization(  features_in, features_out, rank=rank, alpha=lora_alpha, device=device  )  # 使用parametrize.register_parametrization将LoRA参数化应用到指定的网络层  
# 这里我们为net.linear1, net.linear2, net.linear3的权重添加了LoRA参数化  
parametrize.register_parametrization(  net.linear1, "weight", linear_layer_parameterization(net.linear1, device="your_device_here")  
)  
parametrize.register_parametrization(  net.linear2, "weight", linear_layer_parameterization(net.linear2, device="your_device_here")  
)  
parametrize.register_parametrization(  net.linear3, "weight", linear_layer_parameterization(net.linear3, device="your_device_here")  
)  # 注意:上面的代码中 "your_device_here" 需要替换为你的实际设备名,比如 "cuda:0" 或 "cpu"  # 定义一个函数来启用或禁用LoRA参数化  
def enable_disable_lora(enabled=True):  for layer in [net.linear1, net.linear2, net.linear3]:  # 通过访问layer.parametrizations["weight"][0]来访问并修改LoRA参数化的enabled属性  layer.parametrizations["weight"][0].enabled = enabled

在上面的代码中,我们首先定义了一个函数linear_layer_parameterization,它接受一个线性层、设备名称、秩(rank)和LoRA的alpha参数作为输入,并返回一个LoRAParametrization对象。然后,我们使用parametrize.register_parametrization函数将这个参数化应用到网络的特定层的权重上。最后,我们定义了一个enable_disable_lora函数,它允许我们动态地启用或禁用这些层上的LoRA参数化。

请注意,你需要将"your_device_here"替换为适合你实际情况的设备名称,比如"cuda:0"(如果你的GPU可用并且你想在GPU上运行)或"cpu"(如果你想在CPU上运行)。

导入torch.nn.utils.parametrize模块,这个模块提供了参数化网络权重的工具。

定义一个函数linear_layer_parameterization,用于生成LoRA参数化对象。此函数接收一个层对象layer、设备device、秩rank和LoRA参数lora_alpha作为参数,然后返回一个LoRAParametrization实例。

  • 根据论文的4.2节,本研究仅限于仅适应下游任务的注意力权重,并将多层感知器(MLP)模块冻结,这样做既简单又节省参数。
  • 函数中获取层的权重矩阵的形状features_infeatures_out
  • 返回一个LoRAParametrization实例,该实例将用于参数化指定层的权重矩阵。

使用parametrize.register_parametrization函数为网络中的线性层(net.linear1net.linear2net.linear3)的权重注册LoRA参数化。

定义一个函数enable_disable_lora,用于启用或禁用LoRA参数化。

  • 此函数接收一个参数enabled,用于指定是否启用LoRA参数化,默认为True启用。
  • 遍历网络中的线性层,通过layer.parametrizations["weight"][0].enabled设置LoRA参数化的启用状态。

这样,我们就可以在网络的每个线性层上应用LoRA参数化,并根据需要启用或禁用它。

5. 显示由LoRA添加的参数数量。

# 初始化LoRA参数和非LoRA参数的总数  
total_parameters_lora = 0  
total_parameters_non_lora = 0  # 遍历网络中的线性层  
for index, layer in enumerate([net.linear1, net.linear2, net.linear3]):  # 计算LoRA参数(lora_A和lora_B)的数量,并累加到total_parameters_lora  total_parameters_lora += layer.parametrizations["weight"][0].lora_A.nelement() + layer.parametrizations["weight"][0].lora_B.nelement()  # 计算非LoRA参数(权重和偏置)的数量,并累加到total_parameters_non_lora  total_parameters_non_lora += layer.weight.nelement() + layer.bias.nelement()  # 打印每层的权重、偏置以及LoRA参数A和B的形状  print(  f'层 {index+1}: W: {layer.weight.shape} + B: {layer.bias.shape} + Lora_A: {layer.parametrizations["weight"][0].lora_A.shape} + Lora_B: {layer.parametrizations["weight"][0].lora_B.shape}'  )  # 验证非LoRA参数的总数是否与原始网络的参数总数匹配  
# 注意:total_parameters_original需要在代码的其他部分定义  
assert total_parameters_non_lora == total_parameters_original  # 打印原始网络的参数总数  
print(f'原始参数总数: {total_parameters_non_lora:,}')  # 打印原始参数和LoRA参数的总数  
print(f'原始参数 + LoRA参数的总数: {total_parameters_lora + total_parameters_non_lora:,}')  # 打印LoRA引入的参数数量  
print(f'LoRA引入的参数数量: {total_parameters_lora:,}')  # 计算LoRA参数相对于原始参数的增量百分比  
parameters_increment = (total_parameters_lora / total_parameters_non_lora) * 100  
print(f'参数增量: {parameters_increment:.3f}%')

计算并显示通过LoRA技术添加到网络中的参数数量。

  • 初始化total_parameters_loratotal_parameters_non_lora变量,分别用于存储LoRA参数和非LoRA参数的总数。
  • 遍历网络中的每个线性层(net.linear1net.linear2net.linear3):
    • 将每个层的LoRA参数(lora_Alora_B)的元素数量加到total_parameters_lora
    • 将每个层的权重和偏置的元素数量加到total_parameters_non_lora
    • 打印每层的权重、偏置、LoRA权重矩阵A和B的形状。
  • 通过断言检查非LoRA参数的数量是否与原始网络中的参数数量相同,确保没有计算错误。
  • 打印原始网络的总参数数量、添加LoRA后的总参数数量,以及LoRA引入的参数数量。
  • 计算参数增加的比例,并以百分比的形式打印出来,保留三位小数。

这段代码计算并打印出在应用LoRA参数化后,每个线性层增加的LoRA参数(lora_Alora_B)的总数量,同时也计算并验证了非LoRA参数(原始权重和偏置项)的总数量与未应用LoRA之前的总参数量一致。最后,它报告了总体参数量的增加情况,包括原始参数和LoRA参数的总和,以及LoRA单独引入的参数数量及其相对于非LoRA参数的百分比增长。

6. 冻结原始网络的所有参数,仅微调LoRA引入的参数。然后,针对数字9微调模型100个批次。

# 冻结非LoRA参数
for name, param in net.named_parameters():if 'lora' not in name:print(f'Freezing non-LoRA parameter {name}')param.requires_grad = False# 重新加载MNIST数据集,仅保留数字9
mnist_trainset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
exclude_indices = mnist_trainset.targets == 9
mnist_trainset.data = mnist_trainset.data[exclude_indices]
mnist_trainset.targets = mnist_trainset.targets[exclude_indices]
# 为训练创建一个数据加载器
train_loader = torch.utils.data.DataLoader(mnist_trainset, batch_size=10, shuffle=True)# 仅使用LoRA在数字9上训练网络,并且只训练100个批次(希望它能提高数字9的性能)
train(train_loader, net, epochs=1, total_iterations_limit=100)

在这个设置中,我们仅对LoRA引入的参数进行微调,以期望改善模型在数字9上的性能,同时保持原始网络的其他参数不变。我们重新加载了MNIST数据集,并过滤出所有不是数字9的样本,以便我们的模型能够专注于学习数字9的特征。然后,我们创建了一个数据加载器,并使用一个自定义的train函数(需要用户自行定义)来训练网络,限制训练迭代次数为100次。

7. 验证微调没有改变原始权重(仅使用LoRA引入的权重)。

# 检查冻结的参数在微调后仍然不变
assert torch.all(net.linear1.parametrizations.weight.original == original_weights['linear1.weight'])
assert torch.all(net.linear2.parametrizations.weight.original == original_weights['linear2.weight'])
assert torch.all(net.linear3.parametrizations.weight.original == original_weights['linear3.weight'])enable_disable_lora(enabled=True)
# 现在让我们以net.linear1层为例,检查LoRA是否正确应用到模型中,如LoRAParametrization.forward()中定义
# 新的linear1.weight是通过我们的LoRA参数化的"forward"函数获得的
# 原始权重已经移动到了net.linear1.parametrizations.weight.original
# 更多信息在这里:https://pytorch.org/tutorials/intermediate/parametrizations.html#inspecting-a-parametrized-module
assert torch.equal(net.linear1.weight, net.linear1.parametrizations.weight.original + (net.linear1.parametrizations.weight[0].lora_B @ net.linear1.parametrizations.weight[0].lora_A) * net.linear1.parametrizations.weight[0].scale)enable_disable_lora(enabled=False)
# 如果我们禁用LoRA,linear1.weight就是原始的那个
assert torch.equal(net.linear1.weight, original_weights['linear1.weight'])

这段代码首先验证了在微调后,所有原始的权重确实没有被改变。接着,启用LoRA,通过断言检查net.linear1层的权重是否如预期那样由原始权重加上LoRA参数化后的调整构成。最后,当禁用LoRA时,再次验证net.linear1.weight确实恢复为原始权重,确保了LoRA的开关功能正常工作。

8. 启用LoRA进行测试。启用LoRA功能,然后进行测试。数字9应该被分类得更好。

# 使用LoRA启用进行测试  
enable_disable_lora(enabled=True)  
test()  # 假设test()函数用于测试网络并输出相关结果

禁用LoRA进行测试。禁用LoRA功能,然后再次进行测试。准确率和错误计数应该与原始网络相同。

# 使用LoRA禁用进行测试  
enable_disable_lora(enabled=False)  
test()  # 假设test()函数用于测试网络并输出相关结果

[!注意] 您可能会观察到微调对其他标签的准确率产生了影响。这是预期的,因为我们的微调是专门针对数字9进行的。

完整代码

import torch
import torchvision.datasets as datasets
import torchvision.transforms as transforms
import torch.nn as nn
from tqdm import tqdm# Make torch deterministic
_ = torch.manual_seed(0)transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])# Load the MNIST data set
mnist_trainset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
# Create a dataloader for the training
train_loader = torch.utils.data.DataLoader(mnist_trainset, batch_size=10, shuffle=True)# Load the MNIST test set
mnist_testset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(mnist_testset, batch_size=10, shuffle=True)# Define the device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")# Create an overly expensive neural network to classify MNIST digits
# Daddy got money, so I don't care about efficiency
class RichBoyNet(nn.Module):def __init__(self, hidden_size_1=1000, hidden_size_2=2000):super(RichBoyNet,self).__init__()self.linear1 = nn.Linear(28*28, hidden_size_1)self.linear2 = nn.Linear(hidden_size_1, hidden_size_2)self.linear3 = nn.Linear(hidden_size_2, 10)self.relu = nn.ReLU()def forward(self, img):x = img.view(-1, 28*28)x = self.relu(self.linear1(x))x = self.relu(self.linear2(x))x = self.linear3(x)return xnet = RichBoyNet().to(device)def train(train_loader, net, epochs=5, total_iterations_limit=None):cross_el = nn.CrossEntropyLoss()optimizer = torch.optim.Adam(net.parameters(), lr=0.001)total_iterations = 0for epoch in range(epochs):net.train()loss_sum = 0num_iterations = 0data_iterator = tqdm(train_loader, desc=f'Epoch {epoch+1}')if total_iterations_limit is not None:data_iterator.total = total_iterations_limitfor data in data_iterator:num_iterations += 1total_iterations += 1x, y = datax = x.to(device)y = y.to(device)optimizer.zero_grad()output = net(x.view(-1, 28*28))loss = cross_el(output, y)loss_sum += loss.item()avg_loss = loss_sum / num_iterationsdata_iterator.set_postfix(loss=avg_loss)loss.backward()optimizer.step()if total_iterations_limit is not None and total_iterations >= total_iterations_limit:returntrain(train_loader, net, epochs=1)original_weights = {}
for name, param in net.named_parameters():original_weights[name] = param.clone().detach()def test():correct = 0total = 0wrong_counts = [0 for i in range(10)]with torch.no_grad():for data in tqdm(test_loader, desc='Testing'):x, y = datax = x.to(device)y = y.to(device)output = net(x.view(-1, 784))for idx, i in enumerate(output):if torch.argmax(i) == y[idx]:correct +=1else:wrong_counts[y[idx]] +=1total +=1print(f'Accuracy: {round(correct/total, 3)}')for i in range(len(wrong_counts)):print(f'wrong counts for the digit {i}: {wrong_counts[i]}')test()# Print the size of the weights matrices of the network
# Save the count of the total number of parameters
total_parameters_original = 0
for index, layer in enumerate([net.linear1, net.linear2, net.linear3]):total_parameters_original += layer.weight.nelement() + layer.bias.nelement()print(f'Layer {index+1}: W: {layer.weight.shape} + B: {layer.bias.shape}')
print(f'Total number of parameters: {total_parameters_original:,}')class LoRAParametrization(nn.Module):def __init__(self, features_in, features_out, rank=1, alpha=1, device='cpu'):super().__init__()# Section 4.1 of the paper:# We use a random Gaussian initialization for A and zero for B, so ∆W = BA is zero at the# beginning of trainingself.lora_A = nn.Parameter(torch.zeros((rank,features_out)).to(device))self.lora_B = nn.Parameter(torch.zeros((features_in, rank)).to(device))nn.init.normal_(self.lora_A, mean=0, std=1)# Section 4.1 of the paper:# We then scale ∆Wx by α/r , where α is a constant in r.# When optimizing with Adam, tuning α is roughly the same as tuning the learning rate if we# scale the initialization appropriately.# As a result, we simply set α to the first r we try and do not tune it.# This scaling helps to reduce the need to retune hyperparameters when we vary r.self.scale = alpha / rankself.enabled = Truedef forward(self, original_weights):if self.enabled:# Return W + (B*A)*scalereturn original_weights + torch.matmul(self.lora_B, self.lora_A).view(original_weights.shape) * self.scaleelse:return original_weightsimport torch.nn.utils.parametrize as parametrizedef linear_layer_parameterization(layer, device, rank=1, lora_alpha=1):# Only add the parameterization to the weight matrix, ignore the Bias# From section 4.2 of the paper:# We limit our study to only adapting the attention weights for downstream tasks and freeze the# MLP modules (so they are not trained in downstream tasks) both for simplicity and# parameter-efficiency.# [...]# We leave the empirical investigation of [...], and biases to a future work.features_in, features_out = layer.weight.shapereturn LoRAParametrization(features_in, features_out, rank=rank, alpha=lora_alpha, device=device)parametrize.register_parametrization(net.linear1, "weight", linear_layer_parameterization(net.linear1, device)
)
parametrize.register_parametrization(net.linear2, "weight", linear_layer_parameterization(net.linear2, device)
)
parametrize.register_parametrization(net.linear3, "weight", linear_layer_parameterization(net.linear3, device)
)def enable_disable_lora(enabled=True):for layer in [net.linear1, net.linear2, net.linear3]:layer.parametrizations["weight"][0].enabled = enabledtotal_parameters_lora = 0
total_parameters_non_lora = 0
for index, layer in enumerate([net.linear1, net.linear2, net.linear3]):total_parameters_lora += layer.parametrizations["weight"][0].lora_A.nelement() + layer.parametrizations["weight"][0].lora_B.nelement()total_parameters_non_lora += layer.weight.nelement() + layer.bias.nelement()print(f'Layer {index+1}: W: {layer.weight.shape} + B: {layer.bias.shape} + Lora_A: {layer.parametrizations["weight"][0].lora_A.shape} + Lora_B: {layer.parametrizations["weight"][0].lora_B.shape}')
# The non-LoRA parameters count must match the original network
assert total_parameters_non_lora == total_parameters_original
print(f'Total number of parameters (original): {total_parameters_non_lora:,}')
print(f'Total number of parameters (original + LoRA): {total_parameters_lora + total_parameters_non_lora:,}')
print(f'Parameters introduced by LoRA: {total_parameters_lora:,}')
parameters_increment = (total_parameters_lora / total_parameters_non_lora) * 100
print(f'Parameters increment: {parameters_increment:.3f}%')# Freeze the non-Lora parameters
for name, param in net.named_parameters():if 'lora' not in name:print(f'Freezing non-LoRA parameter {name}')param.requires_grad = False# Load the MNIST data set again, by keeping only the digit 9
mnist_trainset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
exclude_indices = mnist_trainset.targets == 9
mnist_trainset.data = mnist_trainset.data[exclude_indices]
mnist_trainset.targets = mnist_trainset.targets[exclude_indices]
# Create a dataloader for the training
train_loader = torch.utils.data.DataLoader(mnist_trainset, batch_size=10, shuffle=True)# Train the network with LoRA only on the digit 9 and only for 100 batches (hoping that it would
# improve the performance on the digit 9)
train(train_loader, net, epochs=1, total_iterations_limit=100)# Check that the frozen parameters are still unchanged by the finetuning
assert torch.all(net.linear1.parametrizations.weight.original == original_weights['linear1.weight'])
assert torch.all(net.linear2.parametrizations.weight.original == original_weights['linear2.weight'])
assert torch.all(net.linear3.parametrizations.weight.original == original_weights['linear3.weight'])enable_disable_lora(enabled=True)
# Now let's use layer of net.linear1 as an example to check if the Lora is applied to the model
# correctly as defined in the LoRAParametrization.forward()
# The new linear1.weight is obtained by the "forward" function of our LoRA parametrization
# The original weights have been moved to net.linear1.parametrizations.weight.original
# More info here: https://pytorch.org/tutorials/intermediate/parametrizations.html#inspecting-a-parametrized-module
assert torch.equal(net.linear1.weight, net.linear1.parametrizations.weight.original + (net.linear1.parametrizations.weight[0].lora_B @ net.linear1.parametrizations.weight[0].lora_A) * net.linear1.parametrizations.weight[0].scale)enable_disable_lora(enabled=False)
# If we disable LoRA, the linear1.weight is the original one
assert torch.equal(net.linear1.weight, original_weights['linear1.weight'])# Test with LoRA enabled
enable_disable_lora(enabled=True)
test()# Test with LoRA disabled
enable_disable_lora(enabled=False)
test()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/332422.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型应用:基于Golang实现GPT模型API调用

1.背景 当前OpenAI提供了开放接口,支持通过api的方式调用LLM进行文本推理、图片生成等能力,但目前官方只提供了Python SDK。为了后续更方便集成和应用,可以采用Golang对核心推理调用接口进行封装,提供模型调用能力。 2.相关准备…

MDK安装

MDK安装 1 MDK的差异2 切换MDK3 安装芯片支持包注意点 1 MDK的差异 不同版本MDK有略微的差别,比如:MDK536.EXE,支持版本5的交叉编译链。如下图所示: 而MDK539.EXE不支持版本5的交叉编译链,所以工作的时候&#xff0c…

方正畅享全媒体新闻采编系统 binary.do SQL注入漏洞复现

0x01 产品简介 方正畅享全媒体新闻生产系统是以内容资产为核心的智能化融合媒体业务平台,融合了报、网、端、微、自媒体分发平台等全渠道内容。该平台由协调指挥调度、数据资源聚合、融合生产、全渠道发布、智能传播分析、融合考核等多个平台组成,贯穿新闻生产策、采、编、发…

Docker提示某网络不存在如何解决,添加完网络之后如何删除?

Docker提示某网络不存在如何解决? 创建 Docker 网络 假设现在需要创建一个名为my-mysql-network的网络 docker network create my-mysql-network运行容器 创建网络之后,再运行 mysqld_exporter 容器。完整命令如下: docker run -d -p 9104…

压缩能力登顶 小丸工具箱 V1.0 绿色便携版

平常录制视频或下载保存的视频时长往往都很长,很多时候都想要裁剪、 截取出一些“精华片段”保留下来,而不必保存一整个大型视频那么浪费硬盘空间… 但如今手机或电脑上大多数的视频剪辑软件,切割视频一般都要等待很长时间导出或转换&#…

RK3568笔记二十五:RetinaFace人脸检测训练部署

若该文为原创文章,转载请注明原文出处。 一、介绍 Retinaface是来自insightFace的又一力作,基于one-stage的人脸检测网络。RetinaFace是在RetinaNet基础上引申出来的人脸检测框架,所以大致结构和RetinaNet非常像。 官方提供两种主干特征提取网…

struct.unpack_from()学习笔记

struct.unpack_from(fmt,b_data,offset) 按照指定的格式fmt,从偏移位置offset,对b_data开始解包,返回数据格式是一个元组(v1,v2…) fmt可以有: _struct.py: The remaining chars indicate types of args and must match exactly;…

1738. 找出第 K 大的异或坐标值

题目&#xff1a; 给你一个二维矩阵 matrix 和一个整数 k &#xff0c;矩阵大小为 m x n 由非负整数组成。 矩阵中坐标 (a, b) 的 值 可由对所有满足 0 < i < a < m 且 0 < j < b < n 的元素 matrix[i][j]&#xff08;下标从 0 开始计数&#xff09;执行异…

Springboot整合Minio对象存储超级详细讲解以及配置搭建

windows环境下搭建minio步骤 1.从minio官网进行查看详细信息 地址&#xff1a;https://min.io/里面有详细的配置信息搭建成功之后如下如所示&#xff1a;用户名密码默认情况下为 username&#xff1a;minioadmin password&#xff1a;minioadmin2.搭建成功之后的访问 地址&…

Jenkins + github 自动化部署配置

1 Jenkins安装 AWS EC2安装Jenkins&#xff1a;AWS EC2 JDK11 Jenkins-CSDN博客 AWS EC2上Docker安装Jenkins&#xff1a;https://blog.csdn.net/hhujjj2005/article/details/139078402 2 登录jenkins http://192.168.1.128:8080/ $ docker exec -it d1851d9e3386 /bin/ba…

避免锁表:为Update语句中的Where条件添加索引字段

最近在灰度环境中遇到一个问题&#xff1a;某项业务在创建数据时耗时异常长&#xff0c;但同样的代码在预发环境中并未出现此问题。起初我们以为是调用第三方接口导致的性能问题&#xff0c;但通过日志分析发现第三方接口的响应时间正常。最终&#xff0c;我们发现工单表的数据…

VSCODE gcc运行多个.c文件

一、简介 很多时候&#xff0c;开发者需要使用VSCODE进行C语言算法验证。而VSCODE的gcc编译&#xff0c;默认是只编译本文件的内容&#xff0c;其他.c文件是不参与编译的。这就给开发者带来很大的困扰&#xff0c;因为开发者不可能把所有的算法都写在一个.c文件&#xff0c;特别…

visual studio 2022 ssh 主机密钥算法失败问题解决

 Solution - aengusjiang 问题&#xff1a; I follow the document, then check sshd_config, uncomment“HostKey /etc/ssh/ssh_host_ecdsa_key” maybe need add the key algorithms: #HostKeyAlgorithms ssh-ed25519[Redacted][Redacted]rsa-sha2-256,rsa-sha2-512 Ho…

透视App投放效果,Xinstall助力精准分析,让每一分投入都物超所值!

在移动互联网时代&#xff0c;App的推广与投放成为了每一个开发者和广告主必须面对的问题。然而&#xff0c;如何精准地掌握投放效果&#xff0c;让每一分投入都物超所值&#xff0c;却是一个令人头疼的难题。今天&#xff0c;我们就来谈谈如何通过Xinstall这个专业的App全渠道…

Shell字符串变量

目标 能够使用字符串的3种方式 掌握Shell字符串拼接 掌握shell字符串截取的常用格式 能够定义Shell索引数组和关联数组 能够使用内置命令alias,echo,read,exit,declare操作 掌握Shell的运算符操作 Shell字符串变量 介绍 字符串&#xff08;String&#xff09;就是一系…

让大模型变得更聪明三个方向

让大模型变得更聪明三个方向 随着人工智能技术的飞速发展&#xff0c;大模型在多个领域展现出了前所未有的能力&#xff0c;但它们仍然面临着理解力、泛化能力和适应性等方面的挑战。那么&#xff0c;如何让大模型变得更聪明呢&#xff1f; 方向一&#xff1a;算法创新 1.1算…

Git--本地仓库

文章目录 工作区和暂存区工作区&#xff08;Working Directory&#xff09;版本库&#xff08;Repository&#xff09; 初始化git仓库添加文件到版本库步骤 查看修改内容查看工作区和暂存区状态已add文件已修改/新增 的未add文件git跟踪修改原理 查看提交历史版本回退撤销修改撤…

《python编程从入门到实践》day39加更

# 昨日知识点回顾 添加主题、条目 # 今日知识点学习 19.1.3 编辑条目 1.URL模式edit——entry # learning_logs/urls.py ---snip---# 用于编辑条目的页面path(edit_entry/<int:entry_id>/, views.edit_entry, nameedit_entry), ] 2.视图函数edit_entry() # views.py fr…

Docker搭建mysql性能测试环境

OpenEuler使用Docker搭建mysql性能测试环境 一、安装Docker二、docker安装mysql三、测试mysql连接 一、安装Docker 建立源文件vim /etc/yum.repos.d/docker-ce.repo增加内容[docker-ce-stable] nameDocker CE Stable - $basearch baseurlhttps://repo.huaweicloud.com/docker…

SVM原问题与对偶问题

目的&#xff1a;求出我们的f(X)&#xff0c;它代表着我们X映射到多维的情况&#xff0c;能够帮我们在多维中招到超平面进行分类。 1.优化问题&#xff1a; 1.1推荐好书&#xff1a; 1.2 优化理论中的原问题&#xff1a; 原问题和限制条件如下&#xff1a; 这是一个泛化性…