ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

使用 LoRA 与 PEFT 微调自定义多层感知机(MLP):非 Transformers 模型参数高效微调的完整实战指南

2026/9/20 6:57:56 拓冰建站 浏览量
使用 LoRA 与 PEFT 微调自定义多层感知机(MLP):非 Transformers 模型参数高效微调的完整实战指南 人工智能大模型微调LoRA【免费下载链接】peft PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.项目地址https://gitcode.com/gh_mirrors/pe/peft点击查看免费下载PEFTParameter-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 参考文档。二、环境准备安装最新版 PEFTNotebook 的第一步是确保 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 为 64n_train 800 batch_size 64 train_dataloader torch.utils.data.DataLoader( torch.utils.data.TensorDataset(X[:n_train], y[:n_train]), batch_sizebatch_size, shuffleTrue, ) eval_dataloader torch.utils.data.DataLoader( torch.utils.data.TensorDataset(X[n_train:], y[n_train:]), batch_sizebatch_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_hidden2000): 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 cudadef 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(), lrlr) criterion nn.CrossEntropyLoss()%time train(module, optimizer, criterion, train_dataloader, eval_dataloader, epochsmax_epochs)Notebook 中的训练日志节选关键结果epoch0 train_loss_total0.7970 eval_loss_total0.6472 epoch1 train_loss_total0.5597 eval_loss_total0.4898 epoch2 train_loss_total0.3696 eval_loss_total0.3323 epoch3 train_loss_total0.2364 eval_loss_total0.5454 epoch4 train_loss_total0.2428 eval_loss_total0.2843 epoch5 train_loss_total0.1251 eval_loss_total0.2514 ... epoch14 train_loss_total0.0065 eval_loss_total0.2237 ... epoch29 train_loss_total0.0008 eval_loss_total0.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 训练配置 LoRA6.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.3LogSoftmax是seq.5。6.2 定义 LoraConfig示例的 LoRA 配置没有任何特殊之处config peft.LoraConfig( r8, target_modules[seq.0, seq.2], modules_to_save[seq.4], )三个参数的语义结合 LoraConfig 源码 的字段定义参数取值作用r8LoRA 的秩attention dimension即低秩分解矩阵 A/B 的中间维度r越大适配器参数量越多、表达能力越强target_modules[seq.0, seq.2]要注入 LoRA 的模块名列表。匹配规则列表内字符串做精确匹配或以该字符串结尾的匹配传字符串时按正则匹配传all-linear时选择全部nn.Linear/Conv1D层若模型是PreTrainedModel则自动排除输出层。注意如果模型架构未知且未指定target_modulesPEFT 会直接报错提示必须手动指定modules_to_save[seq.4]除适配器层之外还要置为可训练并保存的模块列表。seq.4是输出层示例让它正常全量训练但不加 LoRA。源码注释说明在序列分类/词元分类任务中随机初始化的classifier/score输出层通常就需要通过该参数保持可训练此外值得补充说明在 LoraConfig 中还定义了lora_alpha默认 8缩放因子lora_alpha/r设use_rsloraTrue时改用lora_alpha/sqrt(r)、lora_dropout默认 0.0、biasnone/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.Conv1DGPT-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(), lrlr) 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_gradTrue的参数数量输出格式为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, epochsmax_epochs)Notebook 中的训练日志节选epoch0 train_loss_total0.6695 eval_loss_total0.6388 epoch1 train_loss_total0.5614 eval_loss_total0.5456 epoch2 train_loss_total0.3897 eval_loss_total0.3035 epoch3 train_loss_total0.2529 eval_loss_total0.2510 epoch4 train_loss_total0.1914 eval_loss_total0.2191 epoch5 train_loss_total0.1236 eval_loss_total0.2586 epoch6 train_loss_total0.1076 eval_loss_total0.3205 epoch7 train_loss_total0.1834 eval_loss_total0.3951 epoch8 train_loss_total0.1037 eval_loss_total0.1646 epoch9 train_loss_total0.0724 eval_loss_total0.1409 ... epoch19 train_loss_total0.0011 eval_loss_total0.1984 epoch20 train_loss_total0.0010 eval_loss_total0.1821 ... epoch29 train_loss_total0.0003 eval_loss_total0.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(fNew 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 分解为 A20 × 8 160与 B8 × 2000 16000合计 16160 个参数远小于原矩阵的 40000seq.2是2000 × 2000的矩阵A/B 分别为 16000 与 16000合计 32000对比原权重 4000000 缩小了两个数量级。从源码看这些矩阵正是 LoRA 层实现 中的self.lora_A/self.lora_Bnn.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(fParameter {name_before:13} | {param.numel():7} parameters | not updated) else: print(fParameter {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 推送适配器到 Hubuser 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 IDfrom_pretrained的完整签名见 peft_model.pyloaded 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 注入的底层原理为了让读者不仅会跑而且懂原理这里结合仓库源码梳理关键实现点配置对象LoraConfigsrc/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_modelmapping_func.py#L105-L113根据配置类型分派LoRA 等 tuner 类配置返回PeftModel内部再包裹对应的LoraModelprompt learning 类配置返回直接包装的PeftModel。它会在调用前后校验base_model_name_or_path并在检测到模型已被注入过 tuner 层时发出警告提示如需更换配置先调用.unload()。注入与冻结BaseTunertuners_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_weightsTrue时 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 MBfloat32 全量211 kB基于本示例可以自然延伸到更广阔的场景任意自定义网络只要网络由受支持的层类型Linear/Embedding/Conv2d/Conv1D构成均可按查看named_modules→ 配置target_modules/modules_to_save→get_peft_model三步接入包括视觉模型、图模型、推荐模型等LoRA 变体与高级配置use_doraDoRA、use_rslora秩稳定缩放、init_lora_weightsPiSSA/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.项目地址https://gitcode.com/gh_mirrors/pe/peft点击查看免费下载相关推荐claude-skills 微调实战LoRA、QLoRA 与参数高效微调PEFT完整指南claude skills 微调实战LoRA、QLoRA 与参数高效微调PEFT完整指南 在 claude skills 仓库中 fine tuningAI 技能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大模型人工智能教程本地部署微调上一篇Pixelle-Video终极指南如何用AI全自动短视频引擎革新内容创作下一篇鸣潮自动化助手5分钟解放双手让游戏回归乐趣本质创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考