- 人工智能
- 大模型
- 微调
- LoRA
【免费下载链接】peft
🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.
PEFT(Parameter-Efficient Fine-Tuning)常被等同于"大语言模型微调工具",但其设计初衷远不止于此:只要被微调的层类型受支持,PEFT 可以应用于任意torch.nn.Module模型,并不要求模型来自 Hugging Face Transformers 库。本指南以仓库中的 multilayer_perceptron 示例 及其配套 Notebook multilayer_perceptron_lora.ipynb 为骨架,完整演示如何把一个纯 PyTorch 手写的三层 MLP 通过 LoRA 进行参数高效微调,并涵盖:数据集构造、LoraConfig配置、get_peft_model包装、可训练参数量对比、权重更新核验,以及通过 Hugging Face Hub 推送/加载适配器。读完本文,你将掌握把 PEFT/LoRA 套用到任何自定义 PyTorch 网络的完整方法,并理解其底层原理。
一、核心前提:PEFT 不限于 Transformers 模型
示例 README 开宗明义地指出:
PEFT supports fine-tuning any type of model as long as the layers being used are supported. The model does not have to be a transformers model, for instance.
这是整个示例的立足点。PEFT 的注入机制(BaseTuner/BaseTunerLayer)工作在torch.nn.Module的层级上,通过"找到目标模块 → 用适配器层替换/包装 → 冻结其余权重"的方式工作,因此对模型来自哪个库并不敏感。这一论断在源码中可以得到印证:
get_peft_model的函数签名第一个参数就是model: torch.nn.Module,其 docstring 明确写道:"Typically this is a Transformers model but anynn.Modulecan work, with the caveat that task-specific features (peft_config.task_type) require the model to follow Transformers conventions."(通常是 Transformers 模型,但任何nn.Module都可以,唯一前提是依赖task_type的任务级功能需要模型遵循 Transformers 约定)。- 换句话说,只要不依赖
task_type等 Transformers 专属约定(例如本示例中的纯分类训练,损失函数由用户自己计算),任何自定义网络都可以被 PEFT 包装。
这也意味着本示例的教学价值不止于 MLP 本身:它证明了"PEFT 适配器(adapter)是模型无关的",同一套 LoRA 注入逻辑既能作用于 LLM 的q_proj/v_proj,也能作用于普通nn.Linear。相关的官方文档可进一步参考 custom_models 开发指南 与 LoRA 参考文档。
二、环境准备:安装最新版 PEFT
Notebook 的第一步是确保 PEFT 为最新版本,在 Python 环境中执行:
python -m pip install --upgrade peft随后导入所需依赖(Notebook 中同时设置了BITSANDBYTES_NOWELCOME环境变量以屏蔽 bitsandbytes 的欢迎横幅,该变量与本示例无直接关系,但保留无妨):
import copy import os # ignore bnb warnings os.environ["BITSANDBYTES_NOWELCOME"] = "1" import peft import torch from torch import nn import torch.nn.functional as F为复现结果,固定随机种子:
torch.manual_seed(0)三、构造玩具分类数据集
示例使用一个带"少量信号"的合成数据集,便于直观观察训练过程中损失下降:
X = torch.rand((1000, 20)) y = (X.sum(1) > 10).long()数据集共 1000 个样本,每个样本是 20 维随机向量;标签由规则X.sum(1) > 10生成(二分类,0/1)。前 800 条作为训练集,后 200 条作为验证集,batch size 为 64:
n_train = 800 batch_size = 64 train_dataloader = torch.utils.data.DataLoader( torch.utils.data.TensorDataset(X[:n_train], y[:n_train]), batch_size=batch_size, shuffle=True, ) eval_dataloader = torch.utils.data.DataLoader( torch.utils.data.TensorDataset(X[n_train:], y[n_train:]), batch_size=batch_size, )四、定义多层感知机模型
示例故意使用超大的隐藏层(2000 个隐藏单元),对这个简单的二分类任务来说完全过度设计——但这正是刻意为之:它放大了全参训练的开销,从而更鲜明地衬托出 PEFT 只训练少量参数的优势。Notebook 中的说明是:"In more realistic settings, models will also be quite large on average, so this is not far-fetched."(在更现实的场景中模型平均也相当大,因此这种设定并不牵强。)
class MLP(nn.Module): def __init__(self, num_units_hidden=2000): super().__init__() self.seq = nn.Sequential( nn.Linear(20, num_units_hidden), nn.ReLU(), nn.Linear(num_units_hidden, num_units_hidden), nn.ReLU(), nn.Linear(num_units_hidden, 2), nn.LogSoftmax(dim=-1), ) def forward(self, X): return self.seq(X)网络结构为20 → 2000 → 2000 → 2,中间两个nn.Linear(分别对应seq.0与seq.2)加上输出层seq.4,共约404 万个参数——其中仅seq.2的权重矩阵就有 2000 × 2000 = 400 万。这也是后面"只训练约 1% 参数"这一结论的计算基础。
五、训练循环与超参数
训练超参数与训练/评估函数如下:
lr = 0.002 batch_size = 64 max_epochs = 30 device = torch.accelerator.current_accelerator().type if hasattr(torch, "accelerator") else "cuda"def train(model, optimizer, criterion, train_dataloader, eval_dataloader, epochs): for epoch in range(epochs): model.train() train_loss = 0 for xb, yb in train_dataloader: xb = xb.to(device) yb = yb.to(device) outputs = model(xb) loss = criterion(outputs, yb) train_loss += loss.detach().float() loss.backward() optimizer.step() optimizer.zero_grad() model.eval() eval_loss = 0 for xb, yb in eval_dataloader: xb = xb.to(device) yb = yb.to(device) with torch.no_grad(): outputs = model(xb) loss = criterion(outputs, yb) eval_loss += loss.detach().float() eval_loss_total = (eval_loss / len(eval_dataloader)).item() train_loss_total = (train_loss / len(train_dataloader)).item() print(f"{epoch=:<2} {train_loss_total=:.4f} {eval_loss_total=:.4f}")注意这里没有使用 Transformers 的Trainer,完全是手写的标准 PyTorch 训练循环——这再次印证"PEFT 与模型库无关":包装后的PeftModel对外仍是一个普通的nn.Module,任何 PyTorch 训练代码都可以直接使用。
5.1 基线实验:不使用 PEFT 的全参训练
先训练一份完全微调的基线,明确"期望中的表现":
module = MLP().to(device) optimizer = torch.optim.Adam(module.parameters(), lr=lr) criterion = nn.CrossEntropyLoss()%time train(module, optimizer, criterion, train_dataloader, eval_dataloader, epochs=max_epochs)Notebook 中的训练日志(节选关键结果):
epoch=0 train_loss_total=0.7970 eval_loss_total=0.6472 epoch=1 train_loss_total=0.5597 eval_loss_total=0.4898 epoch=2 train_loss_total=0.3696 eval_loss_total=0.3323 epoch=3 train_loss_total=0.2364 eval_loss_total=0.5454 epoch=4 train_loss_total=0.2428 eval_loss_total=0.2843 epoch=5 train_loss_total=0.1251 eval_loss_total=0.2514 ... epoch=14 train_loss_total=0.0065 eval_loss_total=0.2237 ... epoch=29 train_loss_total=0.0008 eval_loss_total=0.2633 CPU times: user 1.31 s, sys: 236 ms, total: 1.54 s Wall time: 1.56 s全参基线最终验证损失约为0.26,远优于随机猜测(随机二分类的交叉熵约为 ln2 ≈ 0.693),说明模型确实学到了数据中的规则。训练 30 个 epoch 仅耗时约 1.5 秒(CPU 环境)。
六、使用 PEFT 训练:配置 LoRA
6.1 检查模块命名
PEFT 通过模块名定位目标层,因此第一步是列出模型的命名结构:
[(n, type(m)) for n, m in MLP().named_modules()]输出:
[('', __main__.MLP), ('seq', torch.nn.modules.container.Sequential), ('seq.0', torch.nn.modules.linear.Linear), ('seq.1', torch.nn.modules.activation.ReLU), ('seq.2', torch.nn.modules.linear.Linear), ('seq.3', torch.nn.modules.activation.ReLU), ('seq.4', torch.nn.modules.linear.Linear), ('seq.5', torch.nn.modules.activation.LogSoftmax)]可以看到三个线性层分别是seq.0、seq.2、seq.4,两个 ReLU 是seq.1、seq.3,LogSoftmax是seq.5。
6.2 定义 LoraConfig
示例的 LoRA 配置"没有任何特殊之处":
config = peft.LoraConfig( r=8, target_modules=["seq.0", "seq.2"], modules_to_save=["seq.4"], )三个参数的语义(结合 LoraConfig 源码 的字段定义):
| 参数 | 取值 | 作用 |
|---|---|---|
r | 8 | LoRA 的秩("attention dimension"),即低秩分解矩阵 A/B 的中间维度;r越大,适配器参数量越多、表达能力越强 |
target_modules | ["seq.0", "seq.2"] | 要注入 LoRA 的模块名列表。匹配规则:列表内字符串做精确匹配或以该字符串结尾的匹配;传字符串时按正则匹配;传"all-linear"时选择全部nn.Linear/Conv1D层(若模型是PreTrainedModel则自动排除输出层)。注意:如果模型架构未知且未指定target_modules,PEFT 会直接报错,提示必须手动指定 |
modules_to_save | ["seq.4"] | 除适配器层之外还要"置为可训练并保存"的模块列表。seq.4是输出层,示例让它正常全量训练但不加 LoRA。源码注释说明:在序列分类/词元分类任务中,随机初始化的classifier/score输出层通常就需要通过该参数保持可训练 |
此外值得补充说明:在 LoraConfig 中还定义了lora_alpha(默认 8,缩放因子lora_alpha/r;设use_rslora=True时改用lora_alpha/sqrt(r))、lora_dropout(默认 0.0)、bias("none"/"all"/"lora_only",默认"none")、init_lora_weights(默认True,即 Microsoft 参考实现的初始化:LoRA B 置零,训练前适配器是恒等 no-op;还可选"gaussian"、"pissa"、"olora"、"loftq"等初始化策略)、exclude_modules(排除指定模块)等。本示例仅使用默认值即可。
层类型支持范围:Notebook 特别给出提醒——并非所有层类型都能用 LoRA 微调。目前支持的有:
nn.Linear线性层- Embedding 嵌入层
nn.Conv2dtransformers.pytorch_utils.Conv1D(GPT-2 等使用的 1D 卷积)
(从仓库源码看,LoRA 的 dispatch 机制确实按Linear、Embedding、Conv2d、Conv1D、量化层等类型分别创建对应的LoraLayer子类,见 src/peft/tuners/lora/ 下的layer.py、embedding.py、conv.py等文件。)
6.3 用 get_peft_model 包装模型
module = MLP().to(device) module_copy = copy.deepcopy(module) # we keep a copy of the original model for later peft_model = peft.get_peft_model(module, config) optimizer = torch.optim.Adam(peft_model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() peft_model.print_trainable_parameters()关键点:
get_peft_model(model, config)会对传入的模型原地修改(docstring 明确注明 "the model will be modified in-place"),把目标模块替换为 LoRA 包装层,并冻结除适配器(与modules_to_save)以外的所有权重。- 示例先
copy.deepcopy了一份原始模型module_copy,用于后面做参数对比验证。 print_trainable_parameters()的实现位于 peft_model.py,它调用get_nb_trainable_parameters()统计requires_grad=True的参数数量,输出格式为:
trainable params: {trainable_params:,d} || all params: {all_param:,d} || trainable%: {100 * trainable_params / all_param:.4f}- Notebook 明确说明:可训练参数仅约占全部参数的 1%——这正是 PEFT 追求的效果("only ~1% of parameters are actually trained, which is what we like to see")。
6.4 PEFT 训练结果
%time train(peft_model, optimizer, criterion, train_dataloader, eval_dataloader, epochs=max_epochs)Notebook 中的训练日志(节选):
epoch=0 train_loss_total=0.6695 eval_loss_total=0.6388 epoch=1 train_loss_total=0.5614 eval_loss_total=0.5456 epoch=2 train_loss_total=0.3897 eval_loss_total=0.3035 epoch=3 train_loss_total=0.2529 eval_loss_total=0.2510 epoch=4 train_loss_total=0.1914 eval_loss_total=0.2191 epoch=5 train_loss_total=0.1236 eval_loss_total=0.2586 epoch=6 train_loss_total=0.1076 eval_loss_total=0.3205 epoch=7 train_loss_total=0.1834 eval_loss_total=0.3951 epoch=8 train_loss_total=0.1037 eval_loss_total=0.1646 epoch=9 train_loss_total=0.0724 eval_loss_total=0.1409 ... epoch=19 train_loss_total=0.0011 eval_loss_total=0.1984 epoch=20 train_loss_total=0.0010 eval_loss_total=0.1821 ... epoch=29 train_loss_total=0.0003 eval_loss_total=0.2100 CPU times: user 1.41 s, sys: 48.9 ms, total: 1.46 s Wall time: 1.46 s结论(Notebook 原文的表述):最终验证损失与之前全参训练非常接近(约 0.21 对 0.26,甚至略优),而训练的参数数量却少得多——"This is quite nice to see, given that we are training a much smaller number of parameters."
6.5 验证哪些参数真正被更新
训练结束后,示例通过对比"包装前的原始权重副本module_copy"与"训练后的peft_model.base_model"来精确核验 LoRA 的注入行为。
第一步:列出新增的 LoRA 参数及其规模:
for name, param in peft_model.base_model.named_parameters(): if "lora" not in name: continue print(f"New parameter {name:<13} | {param.numel():>5} parameters | updated")输出:
New parameter model.seq.0.lora_A.default.weight | 160 parameters | updated New parameter model.seq.0.lora_B.default.weight | 16000 parameters | updated New parameter model.seq.2.lora_A.default.weight | 16000 parameters | updated New parameter model.seq.2.lora_B.default.weight | 16000 parameters | updated这里可以直观看到 LoRA 的低秩分解:seq.0是20 × 2000的矩阵,LoRA 分解为 A(20 × 8 = 160)与 B(8 × 2000 = 16000),合计 16160 个参数,远小于原矩阵的 40000;seq.2是2000 × 2000的矩阵,A/B 分别为 16000 与 16000,合计 32000,对比原权重 4000000 缩小了两个数量级。从源码看,这些矩阵正是 LoRA 层实现 中的self.lora_A/self.lora_B(nn.ModuleDict),前向时按scaling = lora_alpha / r对lora_B @ lora_A的结果做缩放后与原始输出相加。
第二步:对比原始权重是否保持不变:
params_before = dict(module_copy.named_parameters()) for name, param in peft_model.base_model.named_parameters(): if "lora" in name: continue name_before = ( name.partition(".")[-1].replace("base_layer.", "").replace("original_", "").replace("module.", "").replace("modules_to_save.default.", "") ) param_before = params_before[name_before] if torch.allclose(param, param_before): print(f"Parameter {name_before:<13} | {param.numel():>7} parameters | not updated") else: print(f"Parameter {name_before:<13} | {param.numel():>7} parameters | updated")输出:
Parameter seq.0.weight | 40000 parameters | not updated Parameter seq.0.bias | 2000 parameters | not updated Parameter seq.2.weight | 4000000 parameters | not updated Parameter seq.2.bias | 2000 parameters | not updated Parameter seq.4.weight | 4000 parameters | not updated Parameter seq.4.bias | 2 parameters | not updated Parameter seq.4.weight | 4000 parameters | updated Parameter seq.4.bias | 2 parameters | updated(第二段循环中seq.4的前两行来自module_copy的原始参数对比,后两行来自peft_model.base_model中modules_to_save包装后的同名参数,二者命名相同但值不同,因此分别打印出 not updated / updated。)
核验结果完美印证了配置意图:
seq.0、seq.2的原始权重与偏置全部冻结(not updated),只训练注入的 LoRA A/B 低秩矩阵;- 唯一的例外是
seq.4(输出层),因为它被列入modules_to_save,所以被正常全量更新; - Notebook 的总结:"除了新增的 LoRA 权重外,只有最后一层被更新。由于 LoRA 权重与最后一层参数相对很少,这带来了巨大的效率提升。"
七、通过 Hugging Face Hub 共享适配器
PEFT 的另一大便利是:即使模型是自定义的nn.Module,也可以无缝对接 Hugging Face Hub 完成适配器的上传、下载与复现校验。
7.1 推送适配器到 Hub
user = "BenjaminB" # put your user name here model_name = "peft-lora-with-custom-model" model_id = f"{user}/{model_name}"peft_model.push_to_hub(model_id);前提是已拥有有效的 Hugging Face 账号并完成登录(huggingface-cli login)。push_to_hub只会上传适配器权重与adapter_config.json(即 LoRA A/B 矩阵和modules_to_save层的权重),而不会上传庞大的基础模型。Notebook 明确指出:
As we can see, the adapter size is only 211 kB.
211 kB—— 这就是整个可训练部分(约 1% 参数)的体量,与之对比的是约 404 万参数的完整模型(仅权重就约 16 MB 的 float32)。这也解释了参数高效微调在存储与分发上的另一层优势。
7.2 从 Hub 加载适配器
加载只需一步,使用PeftModel.from_pretrained,传入基础模型与 Hub 上的 model ID(from_pretrained的完整签名见 peft_model.py):
loaded = peft.PeftModel.from_pretrained(module_copy, model_id) type(loaded)这里传入的module_copy正是前面保存的原始未训练 MLP——因为push_to_hub只保存了适配器,恢复时必须自己提供与之结构完全一致的基础模型(这正是"适配器与基础模型分离"的典型用法)。
7.3 校验加载结果的一致性
用torch.allclose验证"训练后的peft_model"与"重新加载的loaded"是否产生完全一致的输出:
y_peft = peft_model(X.to(device)) y_loaded = loaded(X.to(device)) torch.allclose(y_peft, y_loaded)若返回True,说明推送/加载链路无误,适配器权重被精确保留。
7.4 清理仓库
作为收尾,可以删除测试用的 Hub 仓库:
from huggingface_hub import delete_repo delete_repo(model_id)八、从源码看 LoRA 注入的底层原理
为了让读者不仅"会跑"而且"懂原理",这里结合仓库源码梳理关键实现点:
配置对象:
LoraConfig(src/peft/tuners/lora/config.py)继承自PeftConfig,在__post_init__中把peft_type固定为PeftType.LORA,并把target_modules从 list 归一化为 set(见 config.py#L1009-L1019)。本示例用到的r、target_modules、modules_to_save分别对应字段r(默认 8)、target_modules(默认 None,架构未知时必须手动指定,否则报错)、modules_to_save(默认 None)。模型包装:
get_peft_model(mapping_func.py#L105-L113)根据配置类型分派:LoRA 等 tuner 类配置返回PeftModel(内部再包裹对应的LoraModel),prompt learning 类配置返回直接包装的PeftModel。它会在调用前后校验base_model_name_or_path,并在检测到模型已被注入过 tuner 层时发出警告(提示如需更换配置先调用.unload())。注入与冻结:
BaseTuner(tuners_utils.py)负责按target_modules逐层调用_create_and_replace把nn.Linear替换为LoraLayer,再通过_mark_only_adapters_as_trainable冻结非适配器权重、解冻modules_to_save指定的模块;_check_target_module_exists实现了"精确匹配或以传入字符串结尾"的匹配规则。LoRA 层:在 LoRA 层实现 中,每个适配器维护
lora_A、lora_B两个低秩矩阵与scaling缩放系数;前向计算output = base(x) + scaling * (lora_B @ lora_A)(x)。默认初始化(init_lora_weights=True)时 B 置零,因此训练前的 PEFT 模型输出与基础模型完全一致——这也是 6.3 节"包装后即可直接训练、不影响初始行为"的原因。
九、总结与延伸
通过这个 30 个 epoch、耗时约 1.5 秒的微型实验,我们完成了对 PEFT 核心工作流的完整闭环:
| 维度 | 全参训练(基线) | PEFT + LoRA |
|---|---|---|
| 训练参数量 | 约 404 万(100%) | 约 4 万(~1%) |
| 最终验证损失 | ≈ 0.26 | ≈ 0.21(接近甚至略优) |
| 权重更新范围 | 全部 | 仅seq.0/seq.2的 LoRA A/B +seq.4输出层 |
| 适配器存储体积 | 约 16 MB(float32 全量) | 211 kB |
基于本示例,可以自然延伸到更广阔的场景:
- 任意自定义网络:只要网络由受支持的层类型(Linear/Embedding/Conv2d/Conv1D)构成,均可按"查看
named_modules→ 配置target_modules/modules_to_save→get_peft_model"三步接入,包括视觉模型、图模型、推荐模型等; - LoRA 变体与高级配置:
use_dora(DoRA)、use_rslora(秩稳定缩放)、init_lora_weights(PiSSA/OLoRA/LoftQ 等初始化)、lora_dropout、bias策略等都可以在同一个LoraConfig中开启(见 LoraConfig 参考文档); - 适配器生态:
push_to_hub/from_pretrained让自定义模型的适配器也能享受 Hub 的版本管理与协作分发; - 更多示例:仓库的 examples 目录 收录了覆盖语言建模、图像生成、语义分割等场景的大量 PEFT 实战脚本,其中 multilayer_perceptron 示例 是最小可复现的"自定义模型 + LoRA"入门范本。
动手建议:直接打开 multilayer_perceptron_lora.ipynb 逐 cell 运行,然后尝试修改r(如 4/16/32)、换用target_modules=["seq.4"]或把所有 Linear 层加入target_modules,观察可训练参数量与验证损失的权衡——你会对"低秩适配"的威力获得最直观的感受。
- 人工智能
- 大模型
- 微调
- LoRA
【免费下载链接】peft
🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.
相关推荐
claude-skills 微调实战:LoRA、QLoRA 与参数高效微调(PEFT)完整指南
claude skills 微调实战:LoRA、QLoRA 与参数高效微调(PEFT)完整指南 在 claude skills 仓库中, fine tuning
AI 技能AI 插件后端前端DevOpsTimesFM 2.5 LoRA 参数高效微调实战:基于 HuggingFace Transformers 与 PEFT 的完整指南
TimesFM 2.5 LoRA 参数高效微调实战:基于 HuggingFace Transformers 与 PEFT 的完整指南 本指南系统讲解如何在当前仓
人工智能基础模型大模型时序预测微调BlueLM-7B-Chat LoRA 微调实战:基于 transformers 与 peft 的高效指令微调完整指南
BlueLM 7B Chat LoRA 微调实战:基于 transformers 与 peft 的高效指令微调完整指南 导读 本文围绕 Datawhale se
大模型人工智能教程本地部署微调
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考