首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

HW8-补充1:在大模型中快速应用 LoRA

  • 25-02-16 23:02
  • 2795
  • 8908
blog.csdn.net

目录

0 前言

1 PEFT 和 LoRA 的关系

2 在大模型中应用 LoRA

2.1 安装必要的库

2.2 加载预训练模型

2.3 应用 LoRA

2.4 查看当前模型架构

2.5 查看增加的参数量

2.5.1 理论计算

2.5.2 使用 PEFT 查看参数

2.5.3 自定义函数查看参数

2.6 准备数据并进行微调

2.7 保存和加载 LoRA 微调的模型

2.7.1 合并 LoRA 权重并卸载 PEFT 包装

3 可能的错误及解决方案(TypeError: Expected state_dict to be dict-like...)

3.1 错误原因

3.2 错误重现

3.3 解决方法

4 一个导致微调看似无效的 Bug:应用 LoRA 前使用 get_peft_model()

5 参考链接


0 前言

本文为李宏毅学习笔记——2024春《GENERATIVE AI》篇——作业笔记HW8-补充1。

如果你还没获取到LLM API,请查看我的另一篇笔记:

HW1~2:LLM API获取步骤及LLM API使用演示:环境配置与多轮对话演示-CSDN博客

完整内容参见:

李宏毅学习笔记——2024春《GENERATIVE AI》篇

如果对 LoRA 还没有一个直观的概念,可以回看这篇文章:

HW4-补充1:认识 LoRA:从线性层到注意力机制-CSDN博客

我们将在这里进一步探讨如何快速地在大型预训练模型中应用 LoRA,并解答可能存在的问题,包括:

  • peft 和 lora 之间有什么关系?
  • get_peft_model 怎么使用?
  • 如何知道应用 LoRA 后模型的参数变化量?
  • 如何使用 merge_and_unload() 合并 LoRA 权重?
  • 认识报错:TypeError: Expected state_dict to be dict-like...
  • 认知一个非常刁钻的 Bug:应用 LoRA 前使用 get_peft_model()。

关于使用LoRA微调的例子,也可以参见我之前的一篇文章:

3.5 Lora 原理与实战-CSDN博客

1 PEFT 和 LoRA 的关系

PEFT(Parameter-Efficient Fine-Tuning)是 Hugging Face 提供的专门用于参数高效微调的工具库。LoRA(Low-Rank Adaptation)是 PEFT 支持的多种微调方法之一,旨在通过减少可训练参数来提高微调大模型的效率。除此之外,PEFT 还支持其他几种常见的微调方法,包括:

  • Prefix-Tuning:冻结原模型参数,为每一层添加可学习的前缀向量,只学习前缀参数。
  • Adapter-Tuning:冻结原模型参数,在模型的层与层之间插入小型的 adapter 模块,仅对 adapter 模块进行训练。
  • ...

2 在大模型中应用 LoRA

下面,我们以实际的例子来展示如何在大模型中快速应用 LoRA。

2.1 安装必要的库

首先,确保你已经安装了 transformers 和 peft 库。

pip install transformers peft

2.2 加载预训练模型

我们以 Hugging Face 的 transformers 库为例,加载一个预训练的 GPT-2 模型,其参数大小为 110M。

  1. from transformers import AutoTokenizer, AutoModelForCausalLM
  2. # 加载预训练的 GPT-2 模型和分词器
  3. tokenizer = AutoTokenizer.from_pretrained('gpt2')
  4. model = AutoModelForCausalLM.from_pretrained('gpt2')
  5. print(model)

打印 model,方便和应用 LoRA 后进行对比。 

2.3 应用 LoRA

使用 peft 库,我们可以轻松地将 LoRA 集成到模型中:

  1. from peft import get_peft_model, LoraConfig, TaskType
  2. # 配置 LoRA
  3. lora_config = LoraConfig(
  4. task_type=TaskType.CAUSAL_LM, # 任务类型:因果语言模型
  5. inference_mode=False, # 推理模式关闭,以进行训练
  6. r=8, # 低秩值 r
  7. lora_alpha=32, # LoRA 的缩放因子
  8. lora_dropout=0.1, # Dropout 概率
  9. )
  10. # 将 LoRA 应用到模型中
  11. model = get_peft_model(model, lora_config)

2.4 查看当前模型架构

print(model)

可以看到 LoRA 已经成功应用。

2.5 查看增加的参数量

应用 LoRA 后,或许你希望了解模型参数量的变化。以下是理论计算和查看方式:

2.5.1 理论计算

对于每个应用了 LoRA 的层,增加的参数量为:

增加的参数量=r×(输入维度+输出维度)

  • r:LoRA 的低秩值。
  • 输入维度:层的输入特征数。
  • 输出维度:层的输出特征数。

2.5.2 使用 PEFT 查看参数

peft 提供了查看模型参数的便捷方法:

  1. # 查看 LoRA 模块
  2. model.print_trainable_parameters()

输出:

trainable params: 294,912 || all params: 124,734,720 || trainable%: 0.23643136409814364

2.5.3 自定义函数查看参数

实际上直接计算所有可训练参数就行。

  1. def print_trainable_parameters(model):
  2. trainable_params = 0
  3. all_params = 0
  4. for _, param in model.named_parameters():
  5. num_params = param.numel()
  6. all_params += num_params
  7. if param.requires_grad:
  8. trainable_params += num_params
  9. print(f"可训练参数量: {trainable_params}")
  10. print(f"总参数量: {all_params}")
  11. print(f"可训练参数占比: {100 * trainable_params / all_params:.2f}%")
  12. print_trainable_parameters(model)

输出:

可训练参数量: 294912
总参数量: 124734720
可训练参数占比: 0.24%

2.6 准备数据并进行微调

假设你已经有了训练数据集 train_dataset,下面是一个简单的样例代码。

  1. from transformers import Trainer, TrainingArguments
  2. # 定义训练参数
  3. training_args = TrainingArguments(
  4. output_dir='./results', # 模型保存和日志输出的目录路径
  5. num_train_epochs=3, # 训练的总轮数(epochs)
  6. per_device_train_batch_size=16, # 每个设备(如GPU或CPU)上的训练批次大小,16表示每次输入模型的数据数量
  7. learning_rate=5e-5, # 学习率
  8. logging_steps=10, # 每隔多少步(steps)进行一次日志记录
  9. save_steps=100, # 每隔多少步保存模型
  10. )
  11. # 创建 Trainer
  12. trainer = Trainer(
  13. model=model, # 训练的模型对象,需要事先加载好
  14. args=training_args, # 上面定义的训练参数配置
  15. train_dataset=train_dataset, # 需要对应替换成已经处理过的dataset
  16. )
  17. # 开始训练
  18. trainer.train()

2.7 保存和加载 LoRA 微调的模型

训练完成后,你可以保存或者加载 LoRA 微调的参数,下面是个简单的示例。

  1. # 保存 LoRA 参数
  2. model.save_pretrained('./lora_model')

在推理时,加载原始的预训练模型和 LoRA 参数。

  1. # 加载原始模型
  2. base_model = AutoModelForCausalLM.from_pretrained("gpt2")
  3. # 加载 LoRA 参数
  4. from peft import PeftModel
  5. model = PeftModel.from_pretrained(base_model, './lora_model')

2.7.1 合并 LoRA 权重并卸载 PEFT 包装

在完成微调后,可以使用 merge_and_unload() 将 LoRA 的权重合并回原始模型。这在部署和推理阶段非常有用,因为这样可以:

  • 减少依赖:合并后,模型成为标准的 transformers 模型,不再需要 peft 库。
  • 提高推理效率:减少了额外的计算开销,推理速度可能会有所提升。
  • 简化模型保存和加载:不需要分别保存基础模型和 LoRA 参数。

运行下面的代码:

  1. # 对比合并前后的模型
  2. print("合并前的模型结构:")
  3. print(model)
  4. # 合并并卸载 LoRA 权重
  5. model = model.merge_and_unload()
  6. print("合并后的模型结构:")
  7. print(model)

输出:

  1. 合并前的模型结构:
  2. PeftModelForCausalLM(
  3. (base_model): LoraModel(
  4. (model): GPT2LMHeadModel(
  5. (transformer): GPT2Model(
  6. (wte): Embedding(50257, 768)
  7. (wpe): Embedding(1024, 768)
  8. (drop): Dropout(p=0.1, inplace=False)
  9. (h): ModuleList(
  10. (0-11): 12 x GPT2Block(
  11. (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  12. (attn): GPT2Attention(
  13. (c_attn): lora.Linear(
  14. (base_layer): Conv1D()
  15. (lora_dropout): ModuleDict(
  16. (default): Dropout(p=0.1, inplace=False)
  17. )
  18. (lora_A): ModuleDict(
  19. (default): Linear(in_features=768, out_features=8, bias=False)
  20. )
  21. (lora_B): ModuleDict(
  22. (default): Linear(in_features=8, out_features=2304, bias=False)
  23. )
  24. (lora_embedding_A): ParameterDict()
  25. (lora_embedding_B): ParameterDict()
  26. )
  27. (c_proj): Conv1D()
  28. (attn_dropout): Dropout(p=0.1, inplace=False)
  29. (resid_dropout): Dropout(p=0.1, inplace=False)
  30. )
  31. (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  32. (mlp): GPT2MLP(
  33. (c_fc): Conv1D()
  34. (c_proj): Conv1D()
  35. (act): NewGELUActivation()
  36. (dropout): Dropout(p=0.1, inplace=False)
  37. )
  38. )
  39. )
  40. (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  41. )
  42. (lm_head): Linear(in_features=768, out_features=50257, bias=False)
  43. )
  44. )
  45. )
  46. 合并后的模型结构:
  47. GPT2LMHeadModel(
  48. (transformer): GPT2Model(
  49. (wte): Embedding(50257, 768)
  50. (wpe): Embedding(1024, 768)
  51. (drop): Dropout(p=0.1, inplace=False)
  52. (h): ModuleList(
  53. (0-11): 12 x GPT2Block(
  54. (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  55. (attn): GPT2Attention(
  56. (c_attn): Conv1D()
  57. (c_proj): Conv1D()
  58. (attn_dropout): Dropout(p=0.1, inplace=False)
  59. (resid_dropout): Dropout(p=0.1, inplace=False)
  60. )
  61. (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  62. (mlp): GPT2MLP(
  63. (c_fc): Conv1D()
  64. (c_proj): Conv1D()
  65. (act): NewGELUActivation()
  66. (dropout): Dropout(p=0.1, inplace=False)
  67. )
  68. )
  69. )
  70. (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  71. )
  72. (lm_head): Linear(in_features=768, out_features=50257, bias=False)
  73. )

 

你应该注意到,合并后模型的 LoRA 层将被去除。

现在,你可以像保存普通模型一样保存:

  1. # 保存合并后的模型
  2. model.save_pretrained('./merged_model')
  3. tokenizer.save_pretrained('./merged_model')

在推理阶段,直接加载这个合并后的模型:

  1. from transformers import AutoModelForCausalLM, AutoTokenizer
  2. # 加载合并后的模型
  3. tokenizer = AutoTokenizer.from_pretrained('./merged_model')
  4. model = AutoModelForCausalLM.from_pretrained('./merged_model')
  5. # 进行推理
  6. inputs = tokenizer("Hello, World!", return_tensors="pt")
  7. outputs = model.generate(**inputs)
  8. print(tokenizer.decode(outputs[0], skip_special_tokens=True))

注意:

  • 不可逆操作:合并操作是不可逆的。如果你之后还需要进一步微调 LoRA 参数,需确保在合并前备份模型。
  • 无需 PEFT 库:合并后的模型不再包含 LoRA 适配器(Adapter)的信息,因此在加载时无需使用 PeftModel。

3 可能的错误及解决方案(TypeError: Expected state_dict to be dict-like...)

在使用 PEFT 和 LoRA 进行模型微调和保存加载时,可能会遇到如下错误:

TypeError: Expected state_dict to be dict-like, got . 

3.1 错误原因

一般是因为混合使用不同的保存和加载方式,这个错误不局限于 PeftModel,问题出在你用 torch.save(model) 保存整个模型却用 load_state_dict() 去加载,注意模型加载和保存的一致性。

3.2 错误重现

下面我们来复现它,看是不是和你的操作一致(这里以 PeftModel 举例):

错误地保存整个 PeftModel 对象而不是其 state_dict:

  1. import torch
  2. from transformers import AutoTokenizer, AutoModelForCausalLM
  3. from peft import get_peft_model, LoraConfig, TaskType
  4. # 加载预训练模型和分词器
  5. tokenizer = AutoTokenizer.from_pretrained('gpt2')
  6. model = AutoModelForCausalLM.from_pretrained('gpt2')
  7. # 配置 LoRA
  8. lora_config = LoraConfig(
  9. task_type=TaskType.CAUSAL_LM,
  10. inference_mode=False,
  11. r=8,
  12. lora_alpha=32,
  13. lora_dropout=0.1,
  14. )
  15. # 应用 LoRA
  16. model = get_peft_model(model, lora_config)
  17. # 错误地保存整个 PeftModel 对象
  18. torch.save(model, './model')

加载时传入PeftModel 对象:

  1. # 初始化模型
  2. model = AutoModelForCausalLM.from_pretrained("gpt2")
  3. # 错误地加载模型,期望接收 state_dict 但实际加载了整个模型对象
  4. model.load_state_dict(torch.load('./model')) # 这里会报错

3.3 解决方法

确保你保存和加载的对象是一致的:

  • torch.save(model, '...') 对应于 torch.load(model, '...')。
  • torch.save(model.state_dict(), '...') 对应于 model.load_state_dict(torch.load('...'))

4 一个导致微调看似无效的 Bug:应用 LoRA 前使用 get_peft_model()

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

  1. # 原始项目代码(正确):
  2. # 将 LoRA 配置应用到 text_encoder 和 unet
  3. text_encoder = get_peft_model(text_encoder, lora_config)
  4. unet = get_peft_model(unet, lora_config)
  5. # 如果设置为继续训练,则加载上一次的模型权重,当然,你可以修改 model_path 来指定其他的路径
  6. if resume:
  7. # 加载上次训练的模型权重,注意这里只加载权重,而不是覆盖整个模型,覆盖:model = torch.load(...)
  8. text_encoder = torch.load(os.path.join(model_path, "text_encoder.pt"))
  9. unet = torch.load(os.path.join(model_path, "unet.pt"))

转换为 PEFT 形式:

  1. # 错误的示范
  2. # 将 LoRA 配置应用到 text_encoder 和 unet
  3. text_encoder = get_peft_model(text_encoder, lora_config)
  4. unet = get_peft_model(unet, lora_config)
  5. # 如果设置为继续训练,则加载上一次的模型权重
  6. if resume:
  7. # 使用 PEFT 的 from_pretrained 方法加载 LoRA 模型
  8. text_encoder = PeftModel.from_pretrained(text_encoder, os.path.join(model_path, "text_encoder"))
  9. unet = PeftModel.from_pretrained(unet, os.path.join(model_path, "unet"))

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

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

  1. import torch
  2. import torch.nn as nn
  3. from torch.optim import Adam
  4. from copy import deepcopy
  5. from peft import get_peft_model, LoraConfig, PeftModel
  6. # 固定随机数种子,确保结果可复现
  7. torch.manual_seed(42)
  8. # 定义一个简单的线性模型
  9. class LinearModel(nn.Module):
  10. def __init__(self, input_size, output_size):
  11. super(LinearModel, self).__init__()
  12. self.linear = nn.Linear(input_size, output_size)
  13. def forward(self, x):
  14. return self.linear(x)
  15. # 实例化线性模型
  16. model = LinearModel(input_size=10, output_size=1)
  17. # 在应用 LoRA 之前深拷贝原始模型,确保后续公平比较
  18. original_model = deepcopy(model)
  19. # 配置 LoRA 参数
  20. config = LoraConfig(
  21. inference_mode=False,
  22. r=4,
  23. lora_alpha=16,
  24. target_modules=['linear'],
  25. )
  26. # 将 LoRA 应用到模型中
  27. lora_model = get_peft_model(model, config)
  28. # 定义一个简单的损失函数和优化器
  29. criterion = nn.MSELoss()
  30. optimizer = Adam(lora_model.parameters(), lr=1e-3)
  31. # 生成一些模拟的训练数据
  32. input_data = torch.randn(100, 10) # 100 个样本,每个样本有 10 个特征
  33. target_data = torch.randn(100, 1) # 对应的目标值
  34. # 训练一个回合
  35. lora_model.train()
  36. for epoch in range(1): # 训练 1 个回合
  37. optimizer.zero_grad()
  38. outputs = lora_model(input_data)
  39. loss = criterion(outputs, target_data)
  40. loss.backward()
  41. optimizer.step()
  42. # 训练后保存 LoRA 权重
  43. lora_model.save_pretrained('linear_lora_model')
  44. # 方法 1:先使用 get_peft_model,再加载 LoRA 权重
  45. model1 = PeftModel.from_pretrained(get_peft_model(deepcopy(original_model), config), 'linear_lora_model')
  46. # 方法 2:直接加载 LoRA 权重
  47. model2 = PeftModel.from_pretrained(deepcopy(original_model), 'linear_lora_model')
  48. # 生成相同的输入数据以进行输出比较
  49. test_input = torch.randn(1, 10)
  50. # 比较四个模型的输出(原始模型,LoRA,方法1,方法2)
  51. def compare_model_outputs(input_data):
  52. # 原始模型
  53. original_output = original_model(input_data)
  54. print("原始模型输出:", original_output.detach().numpy())
  55. # 训练后的 LoRA 模型
  56. lora_output = lora_model(input_data)
  57. print("训练后的 LoRA 模型输出:", lora_output.detach().numpy())
  58. # 方法 1:先使用 get_peft_model,再加载 LoRA
  59. output1 = model1(input_data)
  60. print("方法 1(先使用 get_peft_model,再加载 LoRA)输出:", output1.detach().numpy())
  61. # 方法 2:直接加载 LoRA
  62. output2 = model2(input_data)
  63. print("方法 2(直接加载 LoRA)输出:", output2.detach().numpy())
  64. if torch.allclose(original_output, output1):
  65. print("\n原始模型和方法 1 输出相同。")
  66. if torch.allclose(lora_output, output2):
  67. print("训练后的 LoRA 模型和方法 2 输出相同。\n")
  68. # 比较两个模型的参数
  69. def compare_params(m1, m2):
  70. for (n1, p1), (n2, p2) in zip(m1.named_parameters(), m2.named_parameters()):
  71. if n1 != n2 or not torch.allclose(p1, p2):
  72. print(f"参数不匹配: \n{n1}\n{n2}")
  73. return False
  74. return True
  75. # 比较四个模型的输出
  76. compare_model_outputs(test_input)
  77. # 检查方法 1 和方法 2 的参数是否一致
  78. if compare_params(model1, model2):
  79. print("方法 1 和方法 2 的 LoRA 模型参数一致!")
  80. else:
  81. print("方法 1 和方法 2 的 LoRA 模型参数不一致!")

输出:

  1. 原始模型输出: [[-0.03600371]]
  2. 训练后的 LoRA 模型输出: [[-0.03428639]]
  3. 方法 1(先使用 get_peft_model,再加载 LoRA)输出: [[-0.03600371]]
  4. 方法 2(直接加载 LoRA)输出: [[-0.03428639]]
  5. 原始模型和方法 1 输出相同。
  6. 训练后的 LoRA 模型和方法 2 输出相同。
  7. 参数不匹配:
  8. base_model.model.base_model.model.linear.base_layer.weight
  9. base_model.model.linear.base_layer.weight
  10. 方法 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 应用失效。我已经向官方提出了 issue#2115,并得到了很积极的回复,这些开发人员很棒。预计在未来的版本会解决这个问题,当前 Bug 会出现在版本 <=0.12.0。

5 参考链接

PEFT - Hugging Face

注:本文转载自blog.csdn.net的笨笨sg的文章"https://blog.csdn.net/a131529/article/details/144315610"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top