使用 LoRA 与 PEFT 微调自定义多层感知机(MLP):非 Transformers 模型参数高效微调的完整实战指南
2026/9/20 6:57:49 网站建设 项目流程
  • 人工智能
  • 大模型
  • 微调
  • LoRA

【免费下载链接】peft

🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.

项目地址:https://gitcode.com/gh_mirrors/pe/peft
点击查看免费下载

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.0seq.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.0seq.2seq.4,两个 ReLU 是seq.1seq.3LogSoftmaxseq.5

6.2 定义 LoraConfig

示例的 LoRA 配置"没有任何特殊之处":

config = peft.LoraConfig( r=8, 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_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.Conv2d
  • transformers.pytorch_utils.Conv1D(GPT-2 等使用的 1D 卷积)

(从仓库源码看,LoRA 的 dispatch 机制确实按LinearEmbeddingConv2dConv1D、量化层等类型分别创建对应的LoraLayer子类,见 src/peft/tuners/lora/ 下的layer.pyembedding.pyconv.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.020 × 2000的矩阵,LoRA 分解为 A(20 × 8 = 160)与 B(8 × 2000 = 16000),合计 16160 个参数,远小于原矩阵的 40000;seq.22000 × 2000的矩阵,A/B 分别为 16000 与 16000,合计 32000,对比原权重 4000000 缩小了两个数量级。从源码看,这些矩阵正是 LoRA 层实现 中的self.lora_A/self.lora_Bnn.ModuleDict),前向时按scaling = lora_alpha / rlora_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_modelmodules_to_save包装后的同名参数,二者命名相同但值不同,因此分别打印出 not updated / updated。)

核验结果完美印证了配置意图:

  • seq.0seq.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 注入的底层原理

为了让读者不仅"会跑"而且"懂原理",这里结合仓库源码梳理关键实现点:

  1. 配置对象LoraConfig(src/peft/tuners/lora/config.py)继承自PeftConfig,在__post_init__中把peft_type固定为PeftType.LORA,并把target_modules从 list 归一化为 set(见 config.py#L1009-L1019)。本示例用到的rtarget_modulesmodules_to_save分别对应字段r(默认 8)、target_modules(默认 None,架构未知时必须手动指定,否则报错)、modules_to_save(默认 None)。

  2. 模型包装get_peft_model(mapping_func.py#L105-L113)根据配置类型分派:LoRA 等 tuner 类配置返回PeftModel(内部再包裹对应的LoraModel),prompt learning 类配置返回直接包装的PeftModel。它会在调用前后校验base_model_name_or_path,并在检测到模型已被注入过 tuner 层时发出警告(提示如需更换配置先调用.unload())。

  3. 注入与冻结BaseTuner(tuners_utils.py)负责按target_modules逐层调用_create_and_replacenn.Linear替换为LoraLayer,再通过_mark_only_adapters_as_trainable冻结非适配器权重、解冻modules_to_save指定的模块;_check_target_module_exists实现了"精确匹配或以传入字符串结尾"的匹配规则。

  4. LoRA 层:在 LoRA 层实现 中,每个适配器维护lora_Alora_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_saveget_peft_model"三步接入,包括视觉模型、图模型、推荐模型等;
  • LoRA 变体与高级配置use_dora(DoRA)、use_rslora(秩稳定缩放)、init_lora_weights(PiSSA/OLoRA/LoftQ 等初始化)、lora_dropoutbias策略等都可以在同一个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
点击查看免费下载

相关推荐

上一篇:Pixelle-Video终极指南:如何用AI全自动短视频引擎革新内容创作
下一篇:鸣潮自动化助手:5分钟解放双手,让游戏回归乐趣本质

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询