☰
Mac Air本地微调实战:LoRA+Banking77意图分类30分钟搞定
2026/9/28 16:42:13 网站建设 项目流程

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 python=3.10 conda activate jev-lora

Python 版本选 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.py

models/放原始模型,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 上训练时间会明显拉长,而且收益递减。我最终用的是秩 16,alpha 设为 32,也就是 alpha 是秩的两倍。这个比例是比较常见的做法,能让 LoRA 更新在初期有足够的影响力。

from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["query", "value"], lora_dropout=0.1, bias="none", task_type="SEQ_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"], truncation=True, padding="max_length", max_length=128 ) train_dataset = dataset["train"].map(preprocess, batched=True) test_dataset = dataset["test"].map(preprocess, batched=True)

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_labels=num_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, average="weighted") } training_args = TrainingArguments( output_dir="output/lora-adapter", 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", report_to="none" ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=test_dataset, compute_metrics=compute_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_labels=77 ) model = PeftModel.from_pretrained(base_model, "output/lora-adapter") model.to(device) model.eval()

然后就可以对新的问句做预测了:

def predict(text): inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128) 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 设了 32,max_length 设了 256,结果训练到一半内存爆了,进程直接退出。后来把 batch size 降到 16,max_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 或 16,max_length 降到 128
loss 不下降学习率太小或 target_modules 不对调大学习率到 2e-4 或 5e-4,扩展 target_modules
准确率卡在 70% 左右模型容量不足或训练轮数不够换更大模型或增加 epoch 到 5
MPS 报错不支持某操作MPS 后端兼容性问题切回 CPU 跑通流程,再定位具体操作
保存的模型加载失败只保存了 LoRA 适配器,没保存基础模型加载时先加载基础模型,再加载适配器
推理速度慢模型在 CPU 上跑确认模型已移到 MPS 设备

5.4 几个容易被忽略的实操心得

第一个心得是关于随机种子的。LoRA 微调的结果对随机种子比较敏感,不同种子跑出来的准确率可能差 2 到 3 个百分点。我建议固定一个种子,比如 42,这样结果可复现。如果要做对比实验,至少跑三个种子取平均。

第二个心得是关于评估频率的。每个 epoch 评估一次就够了,太频繁会拖慢训练。如果数据量很大,可以每几百步评估一次。

第三个心得是关于模型保存的。load_best_model_at_end=True会在训练结束时自动加载验证集上最好的那个 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 分钟”基本吻合。如果你机器配置更高或者数据量更小,时间还能再压缩。

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

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

立即咨询