为什么 LoRA 微调和没有微调的效果一样?在 PEFT <= 0.12.0 下错误使用 get_peft_model()

我将其从《PEFT微调:在大模型中快速应用 LoRA》拆分出来以向你展示单独这个 Bug。
另外,我已经向官方提出了 issue#2115,并得到了很积极的回复,这些开发人员很棒。预计在未来的版本会解决这个问题,当前 Bug 会出现在版本 <=0.12.0。
不过现在,让我们定位到它并解决。

检查你的代码中是否使用了错误的方法:

# 正确
model = PeftModel.from_pretrained(model, PATH)

# 错误
model = get_peft_model(model, lora_config)
model = PeftModel.from_pretrained(model, PATH)

定位结束,没有的话就可以关闭这篇文章了,你的解决方法不在这:)

如果你对我发现它的故事感兴趣,可以继续往下阅读:

我花了三个小时排除了所有可能的问题才找到它,起因:将代码从 load stata_dict 转为 PEFT 以供学习。

# 原始项目代码(正确):
# 将 LoRA 配置应用到 text_encoder 和 unet
text_encoder = get_peft_model(text_encoder, lora_config)
unet = get_peft_model(unet, lora_config)

# 如果设置为继续训练,则加载上一次的模型权重,当然,你可以修改 model_path 来指定其他的路径
if resume:
    # 加载上次训练的模型权重,注意这里只加载权重,而不是覆盖整个模型,覆盖:model = torch.load(...)
    text_encoder = torch.load(os.path.join(model_path, "text_encoder.pt"))
    unet = torch.load(os.path.join(model_path, "unet.pt"))

转换为 PEFT 形式:

# 错误的示范
# 将 LoRA 配置应用到 text_encoder 和 unet
text_encoder = get_peft_model(text_encoder, lora_config)
unet = get_peft_model(unet, lora_config)

# 如果设置为继续训练,则加载上一次的模型权重
if resume:
    # 使用 PEFT 的 from_pretrained 方法加载 LoRA 模型
    text_encoder = PeftModel.from_pretrained(text_encoder, os.path.join(model_path, "text_encoder"))
    unet = PeftModel.from_pretrained(unet, os.path.join(model_path, "unet"))

很好,现在我们获得了一个不会报错,但是效果和没加 LoRA 完全相同的模型,真是太棒了(它真的太刁钻了😡)。

来看看它究竟有什么区别,为了清晰,定义一个简单的线性层进行演示:

import torch
import torch.nn as nn
from torch.optim import Adam
from copy import deepcopy
from peft import get_peft_model, LoraConfig, PeftModel

# 固定随机数种子,确保结果可复现
torch.manual_seed(42)

# 定义一个简单的线性模型
class LinearModel(nn.Module):
    def __init__(self, input_size, output_size):
        super(LinearModel, self).__init__()
        self.linear = nn.Linear(input_size, output_size)

    def forward(self, x):
        return self.linear(x)

# 实例化线性模型
model = LinearModel(input_size=10, output_size=1)

# 在应用 LoRA 之前深拷贝原始模型,确保后续公平比较
original_model = deepcopy(model)

# 配置 LoRA 参数
config = LoraConfig(
    inference_mode=False,
    r=4,
    lora_alpha=16,
    target_modules=['linear'],
)

# 将 LoRA 应用到模型中
lora_model = get_peft_model(model, config)

# 定义一个简单的损失函数和优化器
criterion = nn.MSELoss()
optimizer = Adam(lora_model.parameters(), lr=1e-3)

# 生成一些模拟的训练数据
input_data = torch.randn(100, 10)  # 100 个样本,每个样本有 10 个特征
target_data = torch.randn(100, 1)  # 对应的目标值

# 训练一个回合
lora_model.train()
for epoch in range(1):  # 训练 1 个回合
    optimizer.zero_grad()
    outputs = lora_model(input_data)
    loss = criterion(outputs, target_data)
    loss.backward()
    optimizer.step()

# 训练后保存 LoRA 权重
lora_model.save_pretrained('linear_lora_model')

# 方法 1:先使用 get_peft_model,再加载 LoRA 权重
model1 = PeftModel.from_pretrained(get_peft_model(deepcopy(original_model), config), 'linear_lora_model')

# 方法 2:直接加载 LoRA 权重
model2 = PeftModel.from_pretrained(deepcopy(original_model), 'linear_lora_model')

# 生成相同的输入数据以进行输出比较
test_input = torch.randn(1, 10)

# 比较四个模型的输出(原始模型,LoRA,方法1,方法2)
def compare_model_outputs(input_data):
    # 原始模型
    original_output = original_model(input_data)
    print("原始模型输出:", original_output.detach().numpy())

    # 训练后的 LoRA 模型
    lora_output = lora_model(input_data)
    print("训练后的 LoRA 模型输出:", lora_output.detach().numpy())

    # 方法 1:先使用 get_peft_model,再加载 LoRA
    output1 = model1(input_data)
    print("方法 1(先使用 get_peft_model,再加载 LoRA)输出:", output1.detach().numpy())

    # 方法 2:直接加载 LoRA
    output2 = model2(input_data)
    print("方法 2(直接加载 LoRA)输出:", output2.detach().numpy())

    if torch.allclose(original_output, output1):
        print("\n原始模型和方法 1 输出相同。")
    if torch.allclose(lora_output, output2):
        print("训练后的 LoRA 模型和方法 2 输出相同。\n")

# 比较两个模型的参数
def compare_params(m1, m2):
    for (n1, p1), (n2, p2) in zip(m1.named_parameters(), m2.named_parameters()):
        if n1 != n2 or not torch.allclose(p1, p2):
            print(f"参数不匹配: \n{n1}\n{n2}")
            return False
    return True

# 比较四个模型的输出
compare_model_outputs(test_input)

# 检查方法 1 和方法 2 的参数是否一致
if compare_params(model1, model2):
    print("方法 1 和方法 2 的 LoRA 模型参数一致!")
else:
    print("方法 1 和方法 2 的 LoRA 模型参数不一致!")

输出:

原始模型输出: [[-0.03600371]]
训练后的 LoRA 模型输出: [[-0.03428639]]
方法 1(先使用 get_peft_model,再加载 LoRA)输出: [[-0.03600371]]
方法 2(直接加载 LoRA)输出: [[-0.03428639]]

原始模型和方法 1 输出相同。
训练后的 LoRA 模型和方法 2 输出相同。

参数不匹配: 
base_model.model.base_model.model.linear.base_layer.weight
base_model.model.linear.base_layer.weight
方法 1 和方法 2 的 LoRA 模型参数不一致!

从输出中可以看到,方法 1(在加载 LoRA 之前使用 get_peft_model())与原始模型的输出完全相同,这意味着 LoRA 没有被有效应用。而方法 2(直接使用 PeftModel.from_pretrained() 加载 LoRA 权重)的输出与训练后的 LoRA 模型输出一致,说明被正确加载。

另外,你还可以看到方法 1 的模型架构会多一个 base_model.model 包裹,如果你感兴趣的话可以使用 print(model1) 进一步地查看,这证明了在加载 LoRA 之前使用 get_peft_model() 会干扰模型结构,导致 LoRA 应用失效。

在实战操作中,PEFT库可以用来微调BERT模型,以进行文本情感分类任务。首先,我们需要准备一个包含大量文本和标签的数据集,用于训练和验证BERT模型。然后,我们需要利用PEFT库中提供的工具和接口,将数据集转换成BERT模型可接受的格式,并进行数据预处理,如分词和填充等操作。 接着,我们可以利用PEFT库中提供的预训练模型,加载BERT模型的参数和网络结构,并在数据集上进行微调微调的过程中,我们可以通过调整学习率、批大小和训练轮数等超参数,来优化模型的性能。在每个训练轮数结束后,我们可以利用PEFT库中的评估工具对模型进行评估,以了解模型在验证集上的性能表现。 最后,当模型在验证集上的性能达到满意的水平后,我们可以使用PEFT库提供的保存模型工具,将微调后的BERT模型保存下来,以备在实际应用中使用。通过PEFT库的实战操作,我们可以有效地利用BERT模型进行文本情感分类任务,提高模型的准确性和泛化能力,从而更好地满足实际应用的需求。 PEFT库的实战操作不仅帮助我们更好地理解和使用BERT模型,也为我们提供了一套完整的工具和流程,使得模型训练和应用变得更加简单和高效。 PEFT库实战(一): lora微调BERT(文本情感分类) 的操作流程清晰,易于上手,为我们在文本情感分类任务中的应用提供了有力的支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hoper.J

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值