ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Mac Air本地微调实战:LoRA+Banking77意图分类30分钟搞定

Mac Air本地微调实战:LoRA+Banking77意图分类30分钟搞定 1. 项目缘起与整体思路拆解1.1 为什么要在 Mac Air 上折腾本地微调手里这台 Mac Air 是 M 系列芯片、16GB 统一内存的版本平时写代码、跑脚本都挺顺手但一提到“微调模型”很多人第一反应是“这活儿得靠独立显卡”。我一开始也这么想直到实际跑通之后才发现对于参数量在十亿级别以内的小模型配合 LoRA 这种低秩适配方案Mac Air 完全扛得住而且全程不到 30 分钟。这个项目的核心目标很明确在一台没有独立显卡的轻薄本上用 Python 把一个小型文本分类模型微调成能识别银行客服意图的版本。数据集用的是 Banking77这是一个公开的意图分类数据集包含 77 个细分类别、上万条用户问句非常适合拿来练手。模型侧我选的是 JEV 系列里适合本地部署的小参数版本配合 LoRA 做参数高效微调。为什么是 LoRA 而不是全量微调道理很简单。全量微调要把模型所有参数都更新一遍显存占用和计算量都很大Mac Air 的统一内存虽然带宽不错但总量有限全量微调很容易爆内存。LoRA 的思路是在原始权重旁边挂一对低秩矩阵只训练这对小矩阵原始权重冻结不动。这样一来可训练参数量能降到原来的百分之几甚至千分之几内存占用大幅下降训练速度也快得多。提示LoRA 的核心思想可以用一句话概括——不改原模型只加“补丁”。这个补丁很小但能显著改变模型在特定任务上的表现。1.2 整体方案选型与关键决策整个方案可以拆成四层硬件层、框架层、模型层、数据层。硬件层就是 Mac Air 本身M 系列芯片的 GPU 通过 Metal 接口可以被 PyTorch 调用这就是所谓的 MPS 后端。很多人不知道 Mac 也能跑 GPU 加速其实从 PyTorch 1.12 开始MPS 后端就已经比较可用了。我实测下来MPS 后端在 LoRA 微调场景下的速度大约是 CPU 的 3 到 5 倍虽然比不上独立显卡但胜在方便不用额外配机器。框架层我选的是 Hugging Face 的 transformers peft datasets 三件套。transformers 负责加载模型和分词器peft 负责注入 LoRA 适配器datasets 负责加载和处理 Banking77。这套组合是目前最成熟的方案文档全、社区活跃遇到问题容易找到答案。模型层选 JEV 的小参数版本原因是它在保持不错语义理解能力的同时参数量控制得很好适合在 16GB 内存的机器上跑。具体选哪个版本要看你的任务复杂度Banking77 这种意图分类任务十亿参数以内的模型基本够用。数据层就是 Banking77它自带训练集和测试集每条数据是一个用户问句加一个意图标签。我们需要把它转成模型能吃的格式也就是“文本 标签”的配对并且把标签映射成数字 ID。1.3 时间预算与预期效果标题里说“不到 30 分钟”这个时间是怎么算的我拆一下环境准备大概 5 分钟前提是 Python 和依赖都装好了数据加载和预处理 2 分钟模型加载 3 分钟训练 15 分钟左右评估和保存 3 分钟。加起来差不多 28 分钟留点余量就是 30 分钟以内。预期效果方面Banking77 是一个相对难的数据集77 个类别里有不少语义相近的意图比如“卡片丢失”和“卡片被盗”就是两个不同标签。全量微调的大模型能跑到 90% 以上的准确率我们这个小模型加 LoRA 的方案目标定在 80% 到 85% 之间比较现实。如果低于 75%说明超参需要调如果高于 85%那说明这个模型在这个任务上确实很适配。2. 环境准备与依赖安装的实操细节2.1 Python 环境与核心依赖清单Mac Air 上我建议用 conda 或者 venv 建一个独立环境不要污染系统自带的 Python。我习惯用 conda命令如下conda create -n jev-lora python3.10 conda activate jev-loraPython 版本选 3.10 是因为它在兼容性和性能之间平衡得比较好太新的版本有些库还没跟上太旧的版本又缺少一些特性。核心依赖装这几个pip install torch torchvision torchaudio pip install transformers peft datasets accelerate pip install scikit-learn pandas这里有个关键点PyTorch 要装支持 MPS 后端的版本。从 PyTorch 2.0 开始官方预编译包默认就带 MPS 支持所以直接 pip 装就行。装完之后可以用下面这行代码验证 MPS 是否可用import torch print(torch.backends.mps.is_available()) print(torch.backends.mps.is_built())两个都输出 True 才算正常。如果第一个是 False说明你的 PyTorch 版本不对或者系统版本太低。注意Mac Air 的 MPS 后端在某些操作上还不完善比如某些自定义算子可能不支持。遇到报错时可以先切回 CPU 跑通流程再逐步排查是哪个操作触发了问题。2.2 模型与数据集的获取方式JEV 模型可以从官方渠道获取具体地址和申请方式以官方页面为准。下载下来之后一般是一个文件夹里面包含模型权重、配置文件和分词器文件。我建议把模型放在项目目录下的models/文件夹里方便管理。Banking77 数据集可以通过 Hugging Face datasets 库直接加载from datasets import load_dataset dataset load_dataset(banking77)第一次加载会从网络下载之后会缓存到本地。数据集包含train和test两个 split训练集大约一万条测试集大约三千条。加载完之后可以看一眼数据结构print(dataset[train][0])输出大概是{text: I am still waiting on my card?, label: 11}这样的格式。label 是 0 到 76 的整数对应 77 个意图类别。2.3 目录结构与配置管理我习惯把项目组织成下面这样jev-lora-banking77/ ├── models/ │ └── jev-small/ ├── data/ │ └── cache/ ├── output/ │ └── lora-adapter/ ├── train.py └── config.pymodels/放原始模型data/放数据集缓存output/放训练好的 LoRA 适配器。config.py里集中管理所有超参数这样调参的时候不用满文件找。配置项大概包括base_model路径、train_data和val_data的来源、output_dir、学习率、batch size、训练轮数、LoRA 的秩和 alpha 等。把这些写在一个字典里训练脚本直接读清晰又方便。3. 核心细节解析与 LoRA 参数配置3.1 LoRA 的秩与 alpha 怎么选LoRA 有两个最关键的参数秩rank通常记作 r和 alpha缩放系数。秩决定了低秩矩阵的维度秩越大可训练参数越多模型容量越大但也越容易过拟合。alpha 则控制 LoRA 更新量在总输出中的占比。我的经验是对于 Banking77 这种分类任务秩设在 8 到 16 之间比较合适。秩太小比如 4可能欠拟合秩太大比如 64在 Mac Air 上训练时间会明显拉长而且收益递减。我最终用的是秩 16alpha 设为 32也就是 alpha 是秩的两倍。这个比例是比较常见的做法能让 LoRA 更新在初期有足够的影响力。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, lora_alpha32, target_modules[query, value], lora_dropout0.1, biasnone, task_typeSEQ_CLS )target_modules指定要把 LoRA 挂到哪些层上。对于 Transformer 类模型通常挂在注意力机制的 query 和 value 投影层上效果最好。lora_dropout设 0.1 是为了防止过拟合bias设 none 表示不训练偏置项进一步减少参数量。提示如果你不确定 target_modules 该写什么可以先打印模型的所有模块名找到包含query和value的层名。不同模型的命名可能略有差异。3.2 学习率与 batch size 的搭配逻辑学习率是微调里最敏感的的超参。LoRA 微调的学习率通常比全量微调大一些因为可训练参数少需要更大的步长来推动。我试过 1e-4、2e-4、5e-4 三档最终 2e-4 效果最稳。1e-4 收敛太慢5e-4 在后期容易震荡。batch size 受限于内存。Mac Air 16GB 内存模型本身占一部分数据占一部分剩下的给 batch。我实测 batch size 设 16 能跑设 32 就会触发内存交换速度反而变慢。所以最终用 16配合梯度累积 2 步等效 batch size 就是 32。training_args { learning_rate: 2e-4, per_device_train_batch_size: 16, gradient_accumulation_steps: 2, num_train_epochs: 3, warmup_ratio: 0.1, logging_steps: 50, save_strategy: epoch, evaluation_strategy: epoch, load_best_model_at_end: True, metric_for_best_model: accuracy }warmup_ratio设 0.1 是让学习率在前 10% 的步数里从 0 线性升到设定值避免一开始就大步长导致训练不稳定。训练轮数设 3 是因为 Banking77 数据量不算大3 轮基本能收敛再多容易过拟合。3.3 数据预处理与标签映射Banking77 的标签已经是整数但我们需要确保训练集和测试集的标签空间一致。另外文本需要经过分词器处理转成 input_ids 和 attention_mask。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(models/jev-small) def preprocess(examples): return tokenizer( examples[text], truncationTrue, paddingmax_length, max_length128 ) train_dataset dataset[train].map(preprocess, batchedTrue) test_dataset dataset[test].map(preprocess, batchedTrue)max_length设 128 是因为 Banking77 里的问句普遍不长128 个 token 足够覆盖绝大多数样本。设太大浪费计算设太小会截断信息。你可以先统计一下文本长度的分布再决定这个值。标签方面需要把label列保留其他列在训练时会被模型自动忽略。如果标签不是从 0 开始的连续整数还需要做一个映射。Banking77 本身是连续的所以不用额外处理。4. 实操过程与核心环节实现4.1 模型加载与 LoRA 注入的完整流程第一步是加载基础模型。这里要注意分类任务需要在模型顶部加一个分类头输出维度等于类别数。from transformers import AutoModelForSequenceClassification num_labels 77 model AutoModelForSequenceClassification.from_pretrained( models/jev-small, num_labelsnum_labels )加载完之后把 LoRA 配置注入进去model get_peft_model(model, lora_config) model.print_trainable_parameters()最后一行会打印可训练参数的数量和占比。我这边跑出来大概是 0.5% 左右也就是说 99.5% 的原始参数是冻结的。这就是 LoRA 省资源的根本原因。然后把模型移到 MPS 设备上device torch.device(mps if torch.backends.mps.is_available() else cpu) model.to(device)注意有些模型在 MPS 上加载时可能会遇到不支持的操作如果报错可以先在 CPU 上加载再移到 MPS。另外训练过程中如果遇到内存不足可以尝试减小 batch size 或缩短 max_length。4.2 训练循环与评估指标计算训练用 Hugging Face 的 Trainer 类最省事它封装了训练循环、梯度累积、学习率调度、评估和保存逻辑。from transformers import Trainer, TrainingArguments import numpy as np from sklearn.metrics import accuracy_score, f1_score def compute_metrics(eval_pred): logits, labels eval_pred predictions np.argmax(logits, axis-1) return { accuracy: accuracy_score(labels, predictions), f1: f1_score(labels, predictions, averageweighted) } training_args TrainingArguments( output_diroutput/lora-adapter, learning_rate2e-4, per_device_train_batch_size16, gradient_accumulation_steps2, num_train_epochs3, warmup_ratio0.1, logging_steps50, save_strategyepoch, evaluation_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelaccuracy, report_tonone ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasettest_dataset, compute_metricscompute_metrics ) trainer.train()训练过程中日志会每 50 步打印一次 loss 和学习率。我实测下来第一个 epoch 结束时准确率大概在 70% 左右第二个 epoch 到 80%第三个 epoch 稳定在 83% 上下。整个训练过程在 Mac Air 上跑了大约 15 分钟。评估用的是测试集指标包括准确率和加权 F1。加权 F1 考虑了类别不平衡比单纯看准确率更全面。Banking77 的类别分布相对均匀所以两个指标差距不大。4.3 模型保存与推理验证训练完成后LoRA 适配器会保存到output/lora-adapter目录。注意这里保存的只是 LoRA 的那部分参数不是整个模型。文件大小通常只有几 MB 到几十 MB非常轻量。推理的时候需要先加载基础模型再加载 LoRA 适配器from peft import PeftModel base_model AutoModelForSequenceClassification.from_pretrained( models/jev-small, num_labels77 ) model PeftModel.from_pretrained(base_model, output/lora-adapter) model.to(device) model.eval()然后就可以对新的问句做预测了def predict(text): inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) inputs {k: v.to(device) for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) pred torch.argmax(outputs.logits, dim-1).item() return pred print(predict(I lost my card))我拿几个测试集里的样本试了一下预测结果和真实标签基本一致。有些语义相近的类别会混淆比如“卡片丢失”和“卡片被盗”这也是这个数据集本身的难点。5. 常见问题与排查技巧实录5.1 内存不足与训练中断的应对Mac Air 的内存是统一内存GPU 和 CPU 共享。训练时如果 batch size 太大或者 max_length 太长很容易触发内存交换表现为训练速度骤降甚至进程被杀。我踩过的坑是一开始 batch size 设了 32max_length 设了 256结果训练到一半内存爆了进程直接退出。后来把 batch size 降到 16max_length 降到 128就稳定了。排查思路是这样的先用activity monitor看内存压力如果长期处于黄色或红色就说明内存不够。然后逐步降低 batch size 或 max_length直到内存压力回到绿色。另外可以在训练前先跑一个 batch 的 forward看看峰值内存占用。提示Mac Air 上训练时最好关掉其他占内存的应用浏览器标签页尤其吃内存。我习惯训练时只开终端和编辑器。5.2 损失不下降或准确率卡住的排查如果训练几个 epoch 后 loss 不降、准确率卡在某个值不动通常有几个原因。第一是学习率不对。太大导致震荡太小导致收敛慢。可以先用一个较小的学习率跑几百步看 loss 是否稳定下降再逐步调大。第二是 LoRA 的 target_modules 没选对。如果挂到了不重要的层上LoRA 更新对输出的影响很小模型学不到东西。可以尝试把 target_modules 扩展到更多层比如加上key和dense。第三是数据预处理有问题。比如标签映射错了或者分词器把文本截断得太厉害。可以打印几条预处理后的数据人工检查一下。第四是模型本身不适合这个任务。如果换了几组超参都不行可能需要换一个更大的模型或者不同的模型架构。5.3 常见问题速查表问题现象可能原因解决方法训练时内存爆掉batch size 太大或 max_length 太长降低 batch size 到 8 或 16max_length 降到 128loss 不下降学习率太小或 target_modules 不对调大学习率到 2e-4 或 5e-4扩展 target_modules准确率卡在 70% 左右模型容量不足或训练轮数不够换更大模型或增加 epoch 到 5MPS 报错不支持某操作MPS 后端兼容性问题切回 CPU 跑通流程再定位具体操作保存的模型加载失败只保存了 LoRA 适配器没保存基础模型加载时先加载基础模型再加载适配器推理速度慢模型在 CPU 上跑确认模型已移到 MPS 设备5.4 几个容易被忽略的实操心得第一个心得是关于随机种子的。LoRA 微调的结果对随机种子比较敏感不同种子跑出来的准确率可能差 2 到 3 个百分点。我建议固定一个种子比如 42这样结果可复现。如果要做对比实验至少跑三个种子取平均。第二个心得是关于评估频率的。每个 epoch 评估一次就够了太频繁会拖慢训练。如果数据量很大可以每几百步评估一次。第三个心得是关于模型保存的。load_best_model_at_endTrue会在训练结束时自动加载验证集上最好的那个 checkpoint这个功能很实用但要注意save_strategy和evaluation_strategy要设成一样的否则会报错。第四个心得是关于 LoRA 适配器的合并。训练完之后可以把 LoRA 权重合并回基础模型得到一个完整的模型文件推理时就不用再加载适配器了。合并命令是model.merge_and_unload()合并后的模型可以直接用save_pretrained保存。merged_model model.merge_and_unload() merged_model.save_pretrained(output/merged-model) tokenizer.save_pretrained(output/merged-model)合并后的模型大小和原始模型一样但推理时少了一步加载适配器的操作部署起来更方便。6. 扩展方向与个人体会这套流程跑通之后可以往几个方向扩展。一是换数据集比如换成情感分类、新闻分类或者客服工单分类流程基本一样只需要改num_labels和数据加载部分。二是换模型JEV 系列里更大的版本或者同级别的其他模型都可以试LoRA 的配置可能需要微调。三是调 LoRA 参数比如把秩从 16 调到 32或者把 target_modules 扩展到全连接层看效果有没有提升。我个人在实际操作中的体会是Mac Air 跑 LoRA 微调这件事最大的门槛不是硬件而是环境配置和参数调试。环境配好了参数调对了剩下的就是等训练跑完。第一次跑通之后后面再换任务就是改几行配置的事。最后再分享一个小技巧训练的时候可以用time命令包一下记录实际耗时。我这边跑完三个 epoch 是 14 分 52 秒加上数据加载和模型保存总共 28 分钟左右和标题里说的“不到 30 分钟”基本吻合。如果你机器配置更高或者数据量更小时间还能再压缩。
返回列表