欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

为什么 LoRA 修剪与不修剪效果相同?在 PEFT <= 0.12.0 下错误使用 get_peft_model

最编程 2024-10-01 18:05:26
...

我将其从《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 应用失效。