Skip to content

模型微调从入门到实战:从 LoRA 原理到一条命令逐参数拆解

🕒 Published at:

前面聊 RAG 和 Agent,走的都是"不动模型、在外面想办法"的路子。这一篇聊另一条路——微调(Fine-tuning):直接改造模型本身。这篇由浅到深:先讲清什么时候才该微调(很多人一上来就想微调,其实大可不必),再讲透 LoRA 的原理,然后用一条真实的训练命令逐个参数拆解,最后是评估、部署和踩坑。

一、先想清楚:你真的需要微调吗

这是最重要的一节。微调是成本最高的手段,应该是你的最后选项,不是第一反应。

面对"模型效果不好",你手里其实有三张牌,按代价从小到大:

① 提示词工程:改改怎么说话。零成本、立刻生效。先把这个做到位。

② RAG(检索增强):给它喂知识。适合"模型不知道我的资料"——公司文档、产品手册、最新数据。改知识库就能更新,不用重训。

③ 微调:改造模型本身。适合前两者搞不定的场景。

关键判断:微调擅长教"怎么做",不擅长教"是什么"。

① 提示词改怎么说话成本:几乎为零先做这个格式/语气/简单任务② RAG喂它知识成本:中知识会变就用它私有资料/时效数据③ 微调改造模型本身成本:高(数据+算力)前两者搞不定才上风格/能力/领域语感

该微调的场景:需要稳定的输出风格/格式(提示词能提但总不稳);需要模型掌握领域的"语感"和表达习惯(医疗、法律、客服话术);需要特定的行为模式(固定的思考步骤、专属身份);想用小模型替代大模型降本(把大模型的能力"蒸馏"到小模型上);提示词太长太复杂想固化进模型省 token。

不该微调的场景:只是"模型不知道某些事实"——这是 RAG 的活儿,微调塞知识又贵又容易记错还难更新;数据不足(几十条别想了);需求还在天天变。

一句话知识用 RAG,行为用微调,能用提示词解决的就别动模型。

二、微调的几种类型,先分清楚

术语挺乱,一次理清。

按训练目标分

  • 继续预训练(增量预训练):拿海量领域无标注文本继续做"文字接龙"训练,让模型吸收领域知识和语感。数据量大(GB 级),成本高。
  • SFT(监督微调 / 指令微调)拿「问题→答案」成对的数据教模型"遇到这种问题该这么答"。这是 90% 的人说"微调"时指的东西,也是本文重点。
  • 偏好对齐(DPO / RLHF 等)拿「同一问题的好答案 vs 坏答案」的数据教模型偏好,让它答得更符合人类口味。通常在 SFT 之后做。

一般路径:先 SFT,效果不够再考虑对齐。绝大多数业务场景,SFT 就够了。

按训练参数量分

  • 全参数微调(Full):所有权重都更新。效果上限高,但显存吃到爆(一个 7B 模型全参微调往往要几十上百 G 显存),而且容易灾难性遗忘(学会新的、忘了旧的)。
  • 参数高效微调(PEFT):冻结原模型绝大部分权重,只训练很小一部分新增参数。代表就是 LoRA。显存省一大截,效果却往往接近全参。

现实中的默认选择:LoRA。 显存友好、训练快、不容易遗忘、产物小(几十 MB)、还能随时插拔切换。截图里那条命令用的就是它(--train_type lora)。

三、LoRA 原理:为什么"只训练两个小矩阵"就够了

这是要讲透的核心。用大白话说:

模型里是一堆巨大的权重矩阵 W。全参微调是直接把 W 改了。LoRA 的想法是:别动 W,我在旁边并联一个"补丁"。

具体做法:把 W 冻结(完全不训练),额外加两个很小的矩阵 A 和 B。前向计算时结果变成 W·x + B·A·x——原来的输出,加上一个"修正量"。训练时只更新 A 和 B

输入 x原权重 W🔒 冻结,不训练A降维B升维✏️ 只训练这两个小矩阵(rank 决定它们多"瘦")+输出W·x + B·A·x

为什么这样行得通? 因为研究发现,微调时权重的改变量其实是"低秩"的——它没那么复杂,可以用两个瘦长的小矩阵相乘来近似。A 先把维度压瘦(压到 rank 那么宽),B 再还原回去。这个"瘦腰"的宽度就是 rank(秩)

打个比方:给一件衣服改尺寸,你不用把整件衣服拆了重做(全参微调),只要在关键位置加几条收腰的褶子(LoRA)——用极小的改动达到接近的效果。

好处一串:只训练百分之零点几的参数,显存和时间大降;原权重没动,不容易灾难性遗忘;产物只有几十 MB,一个基座模型可以挂多个 LoRA,随时切换(客服 LoRA、写作 LoRA……);推理时还能把 B·A 合并回 W,做到零额外延迟。

QLoRA 是 LoRA 的省钱版:先把基座模型量化到 4bit 装进显存,再在上面做 LoRA。显存能再降一大截,代价是有一点精度损失和速度损失。显存不够时的救命稻草

四、数据准备:决定成败的一环

微调效果的天花板,是数据决定的。 我把话说死:数据质量 >> 数据数量 >> 调参。很多人花一周调超参,其实花一天洗数据收益大得多。

格式长什么样。 SFT 数据本质是「输入 → 期望输出」的成对数据,现在通常用对话格式:

json
{"messages": [
  {"role": "system", "content": "你是某公司的专业客服。"},
  {"role": "user", "content": "我的机器滤网多久洗一次?"},
  {"role": "assistant", "content": "建议每周清洗一次滤网,取出后用清水冲洗晾干即可。"}
]}

训练时,模型只对 assistant 部分计算 loss(学"该怎么答"),user/system 只作为条件。这是 SFT 的基本机制。

要多少条? 经验值:风格/格式对齐几百到几千条就有明显效果;领域能力通常要几千到几万条。但前提是质量高——1000 条精挑细选的,胜过 10000 条脏数据。脏数据不是没用,是有害:模型会忠实地学会你数据里的错误和坏习惯。

质量把关几条:答案必须正确且风格统一(模型会把不一致学成"随机");覆盖真实场景分布,别全是简单样例;去重(重复样本等于变相加大权重);难例要有(只喂简单的,模型遇到难的还是废)。

一个关键技巧:混入通用数据防遗忘。 如果你只拿自己的几千条业务数据训,模型很容易灾难性遗忘——变成一个只会答业务、别的什么都不会甚至变傻的模型。做法是把通用能力数据和你的业务数据按比例混合一起训。

看截图那条命令里就是这么干的:

bash
--dataset 'swift/Qwen3-SFT-Mixin#2000' 'swift/self-cognition:empty_think#600'

Qwen3-SFT-Mixin#2000 就是混入 2000 条通用 SFT 数据保住原有能力;self-cognition#600 是 600 条自我认知数据(教模型"你是谁、谁开发的")。#N 是采样条数。这个"业务数据 + 通用数据混合"的配比思路,是实战里非常重要的一招。

五、实战:把那条命令逐个参数拆开

现在上硬菜。下面这条是用 ms-swift(阿里开源的训练框架,命令行开箱即用)对 Qwen3-0.6B 做 LoRA 微调的完整命令。我把每一行都注释清楚:

bash
CUDA_VISIBLE_DEVICES=0 swift sft \
  --model Qwen/Qwen3-0.6B \
  --train_type lora \
  --dataset 'swift/Qwen3-SFT-Mixin#2000' 'swift/self-cognition:empty_think#600' \
  --torch_dtype bfloat16 \
  --num_train_epochs 1 \
  --per_device_train_batch_size 1 \
  --per_device_eval_batch_size 1 \
  --learning_rate 1e-4 \
  --lora_rank 8 \
  --lora_alpha 32 \
  --target_modules all-linear \
  --gradient_accumulation_steps 16 \
  --eval_steps 50 \
  --save_steps 50 \
  --save_total_limit 2 \
  --logging_steps 5 \
  --max_length 2048 \
  --output_dir output \
  --warmup_ratio 0.05 \
  --dataloader_num_workers 4 \
  --use_liger_kernel true \
  --load_from_cache_file false \
  --loss_scale ignore_empty_think \
  --model_author swift \
  --model_name swift-robot

逐个说清楚:

CUDA_VISIBLE_DEVICES=0 — 指定用第 0 号 GPU。多卡时可以写 0,1,2,3

--model Qwen/Qwen3-0.6B — 基座模型。0.6B 是个很小的模型,适合入门练手(显存要求低、跑得快)。真实业务常用 7B 左右起步。先用小模型把流程跑通,再换大的,这是很省时间的做法。

--train_type lora — 用 LoRA。可选还有 full(全参)等。

--torch_dtype bfloat16 — 训练精度。bf16 比 fp32 省一半显存、速度快,且比 fp16 数值范围更稳(不容易训崩),现代 GPU 上的默认选择

--num_train_epochs 1 — 训练轮数(整个数据集过几遍)。这个参数最容易出事:太少学不到,太多就过拟合(把训练数据背下来了,遇到新问题反而变差)。SFT 常用 1~3。数据量大时 1 轮就够。

--per_device_train_batch_size 1 + --gradient_accumulation_steps 16 — 这两个要一起看。前者是每张卡一次真正塞几条数据(受显存限制),后者是梯度累积:攒 16 个小批次的梯度再更新一次参数。等效批量 = 1 × 16 × 卡数 = 16。这是"显存不够,用时间换"的经典手法——显存只够 batch=1,但训练效果等同于 batch=16。显存不够就调小 batch_size、调大 accumulation

--learning_rate 1e-4 — 学习率,最关键的超参。太大→训崩(loss 飞了、模型变傻);太小→学不动。经验:LoRA 用 1e-4 附近(因为只训小矩阵,可以大胆点);全参微调要小得多,1e-5 附近。这条命令用 1e-4 正是 LoRA 的典型值。

--lora_rank 8 — LoRA 的"秩",也就是前面说的"瘦腰宽度",决定了补丁的表达能力小(4~8):参数少、省显存、够用于风格对齐,不易过拟合;大(16~64+):能学更复杂的能力,但参数多、更易过拟合。从 8 起步是很稳的选择,学不动再往上加。

--lora_alpha 32 — 缩放系数,控制 LoRA 这个"补丁"的影响力权重(实际生效强度约等于 alpha / rank)。这里 32/8 = 4。经验法则:alpha 设成 rank 的 2~4 倍。这条命令是 4 倍,很常见的配法。

--target_modules all-linear — 给哪些层挂 LoRA。all-linear 表示所有线性层都挂——覆盖最全、效果通常更好(早期常见只挂注意力的 q/v 投影,现在普遍认为全挂更好)。

--max_length 2048 — 单条样本的最大 token 长度,超了会被截断。直接影响显存(显存消耗随长度增长很快)。要根据你数据的实际长度设:设太小会把长样本截断丢信息,设太大白白吃显存。

--warmup_ratio 0.05 — 学习率预热比例:前 5% 的训练步里,学习率从 0 慢慢爬到设定值,之后再衰减。作用是防止一开始就用大学习率把模型冲坏,让训练更稳。0.03~0.1 都常见。

--eval_steps 50 / --save_steps 50 — 每 50 步跑一次验证 / 存一次检查点。存档很重要:训练可能中断,也可能后面几步反而变差,有存档才能回退到最好的那个。

--save_total_limit 2 — 最多保留 2 个检查点,旧的自动删(不然硬盘会被塞满)。

--logging_steps 5 — 每 5 步打一次日志(loss 等),方便盯训练曲线。

--output_dir output — 产物输出目录(LoRA 权重、日志都在这)。

--dataloader_num_workers 4 — 数据加载的并行进程数,喂数据别让 GPU 等着。

--use_liger_kernel true — 启用 Liger Kernel,一个优化过的算子库,省显存 + 提速,基本是白捡的收益。

--load_from_cache_file false — 不用数据预处理缓存。改了数据却发现没生效?八成是缓存——这个参数就是防这个坑的。

--loss_scale ignore_empty_think — 这个是 Qwen3 特有的。Qwen3 支持"思考模式"(会输出思考过程),而自我认知这类数据用的是空思考块,这个参数让训练时忽略空思考部分的 loss,避免模型学歪。

--model_author swift / --model_name swift-robot — 配合自我认知数据用的:告诉模型"你叫 swift-robot,是 swift 开发的"。这就是给模型定制身份的方法。

六、训练时盯什么

看 loss 曲线,这是你唯一的仪表盘:

  • train loss 稳步下降:正常。
  • train loss 降、eval loss 不降反升:典型过拟合信号——它在背答案了。→ 减少 epoch、加数据、降 rank、早停。
  • loss 剧烈震荡或变成 NaN:学习率太大或数据有脏东西。→ 降学习率、检查数据。
  • loss 几乎不动:学习率太小、rank 太小、或数据本身没什么可学的。

别只看 loss。 loss 低不等于好用——最终要用评测集在真实任务上测(这点和 RAG 一样,见评估那篇)。

七、练完了怎么用

① 合并权重(可选但常用):把 LoRA 的 B·A 合并回原模型,得到一个完整的新模型,之后推理和普通模型一样、零额外开销。ms-swift 里有 swift export --merge_lora true 之类的能力。

② 不合并、动态加载:保持基座 + 挂载多个 LoRA,按场景切换。一个基座服务多个业务,省显存、好维护。

③ 部署:合并后的模型可以用 vLLM 等推理框架起服务,对外暴露成 OpenAI 兼容接口——然后你前面那些 LangChain / Agent 的代码,把 base_url 一改就能用上自己的模型了。这就闭环了。

八、怎么判断微调成功了

必须做两件事

① 用评测集测目标能力:准备一批真实任务的题(和评估那篇讲的一样),对比微调前后的得分。没有对比就没有结论。

② 测灾难性遗忘:拿一批和你业务无关的通用问题测一遍。如果微调后模型在通用问题上明显变傻(答不出常识、语无伦次),说明遗忘严重——回去提高通用数据混合比例、降低学习率或 epoch。

这两个都测了,你才敢说"微调成功了"。

九、踩坑清单

  • 一上来就微调。 先试提示词、再试 RAG,真不行再微调。很多需求根本用不着。
  • 拿微调塞知识。 知识频繁变、要溯源、要准确 → 那是 RAG 的活。微调塞知识贵、易记错、难更新。
  • 数据脏还怪模型。 模型会忠实学会你数据里的每一个错误。洗数据的收益远大于调参。
  • 只用自己的数据训。 不混通用数据 → 灾难性遗忘,模型变成"只会背业务的傻子"。
  • epoch 拉太高。 3 轮以上就要警惕过拟合,盯住 eval loss。
  • 学习率照抄全参的值。 LoRA(~1e-4)和全参(~1e-5)差一个数量级,抄错直接训崩。
  • 显存 OOM 就放弃。 手段一大把:调小 batch_size + 调大 gradient_accumulation_steps、缩短 max_length、开梯度检查点、上 QLoRA(4bit)、换更小的基座。
  • 改了数据没生效。 数据缓存,加 --load_from_cache_file false
  • 只看 loss 不看实际效果。 loss 是过程指标,评测集才是结果指标。
  • 直接上大模型练。 先用 0.6B/1.5B 这种小模型把整条流程(数据格式、命令、评估)跑通,再换大的——能省你大量时间和算力。

十、小结

一条主线记住:能用提示词就别用 RAG,能用 RAG 就别微调;真要微调,知识归 RAG、行为归微调

技术上:SFT 是主流入口,LoRA 是默认选择(冻结原权重 + 训练两个低秩小矩阵,省显存、防遗忘、可插拔),显存不够上 QLoRA。数据质量决定上限,业务数据一定要混通用数据防遗忘。超参上手就三个关键:学习率(LoRA ~1e-4)、epoch(1~3,防过拟合)、rank/alpha(8 / 2~4倍起步);显存不够就用 batch=1 + 梯度累积换。最后,一切用评测集说话,并且必须测遗忘

微调不神秘,它就是一门"数据 + 少量关键超参 + 严格评估"的手艺活。把上面这条命令亲手跑通一遍,你对它的理解会比读十篇文章都深。