尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Gemma模型LoRA微调实战:从环境搭建到部署全流程

Gemma模型LoRA微调实战:从环境搭建到部署全流程 前阵子接到一个需求让一个 2B 的 Gemma 模型学会按固定格式输出产品摘要不能自由发挥。我第一反应是直接写prompt硬刚试了一轮发现不行——模型记不住规则偶尔会漏字段。于是老老实实走了微调路线用 Hugging Face 的 transformers peft trl 这套工具链对 Gemma 做了 LoRA 微调。整个流程跑下来从准备数据到部署推理大概花了一个周末效果比 prompt 工程稳得多。这篇东西就是把我那几天的实操过程整理出来。我会把选型思路、环境搭建、数据格式、训练代码、踩坑记录全写清楚每一步都解释为什么这么做。适合手里有一块显卡哪怕是消费级 8GB 显存、懂点 Python、想自己动手微调一个大模型的人。不需要你数学多好但最好知道什么是 loss、什么是梯度——不知道也没关系照着抄也能跑通。1. 微调前的思路盘整为什么是 Gemma为什么用 LoRA1.1 选型理由Gemma 的定位与适用场景Gemma 是 Google 在 2024 年开源的一系列轻量级模型有 2B、7B、27B 几个尺寸每个尺寸都有 base 版本和 instruct 版本。它和 Gemini 共享技术积累但体量小得多单张消费级显卡就能跑起来。我选它有几个现实原因开源许可友好Gemma 的授权协议允许商用具体条款以官方为准对个人开发和中小企业来说没有太高的门槛。指令跟随基础好instruct 版本经过 RLHF 之类的对齐理解指令的能力不错。你要微调的往往是“输出格式”或者“领域知识”而不是从头教它说人话。生态成熟Hugging Face 官方支持 Gemmatransformers 的AutoModelForCausalLM直接能加载不用写自定义模型代码。相比之下如果选 LLaMA 系模型虽然生态更丰富但 LLaMA 3 的最小尺寸也有 8B对显存的要求高不少。Qwen 系列也不错可我当时手里的卡只有 12GB 显存跑 2B 的 Gemma 是最稳的选择。这里补一个关键点你要微调的是 base 版还是 instruct 版如果你想让模型学会特定格式、特定风格用 instruct 版做底座效果更好因为它已经具备基本的按指令做事能力。如果你是想注入领域知识比如法律法规、医学知识那 base 版理论上能学得更“扎实”但训练成本也更高——你不仅要教它知识还要教它怎么把知识说出来。新手建议直接选 instruct 版省事很多。1.2 路线对比全量微调、LoRA、QLoRA 选哪个微调大模型不是只有一条路。最常见的是三条路线我直接列个表对比路线训练参数量显存需求7B 级训练速度效果全量微调100%60GB慢上限最高但极易过拟合LoRA0.1%~1%20GB7B快接近全量微调性价比高QLoRA0.1%~1%8~12GB7B较快效果略逊 LoRA但门槛最低全量微调不仅显存门槛高对数据量的要求也很苛刻。几千条数据去更新几十亿参数结果基本是把模型原有的能力冲得七零八落专业说法叫“灾难性遗忘”。咱们普通人手里的数据量级根本撑不起全量微调。LoRA 的思路是冻结原始权重只在模型里插入一些低秩矩阵可理解为小型“补丁层”训练过程只更新这些补丁。原来 7B 模型要更新 70 亿个参数LoRA 只需要更新几百万个显存和时间的消耗大幅下降同时保留了模型原有的通用能力。QLoRA 则是在 LoRA 的基础上先把原始模型量化到 4bit 再冻结进一步压显存。我这次用的就是 QLoRA因为手头只有一张 12GB 的 3060跑 2B 模型有大量余量跑 7B 也能勉强塞下。如果你显存 16GB 以上纯 LoRA不做量化会更稳。下表是我这次实测的显存占用情况。模型gemma-2b-it 量化4bit NF4 LoRA rank16 max_seq_length1024 per_device_train_batch_size4 实际显存占用约 6.2GB含激活值1.3 硬件门槛与合理预期不少人来问“我 8GB 显存能不能微调”。直接给结论2B 级模型 QLoRA6GB 显存起步8GB 很舒服。7B 级模型 QLoRA建议 12GB 以上8GB 能跑但要牺牲 batch size 和序列长度。27B 级模型 QLoRA至少 24GB且序列长度要压到 512 以内。另外补充一句没有 NVIDIA 显卡的话MacBook 的 M 系列芯片也能跑通过 MPS 后端但训练速度比 3060 慢不少新手别为难自己。如果没有好显卡最简单的办法是先租云 GPU按小时计费那种跑一次实验几十块钱。对效果也要有合理预期。LoRA 微调擅长改变模型的“说话风格”和“听话程度”不擅长灌输大规模新知识。比如你想让模型学会“只输出 JSON”这个它能学会想让模型变成某垂直领域的专家几千条数据远远不够。你需要的效果如果是前者LoRA 是够用的。2. 环境搭建与数据准备把地基打牢2.1 环境依赖与版本组合我直接用了一套被验证过很多次的版本组合不建议贪新。conda create -n gemma-sft python3.10 -y conda activate gemma-sft pip install torch2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install transformers4.38.0 pip install peft0.9.0 pip install datasets2.17.0 pip install accelerate0.27.0 pip install trl0.7.11 pip install bitsandbytes0.43.0 pip install huggingface_hub pip install tensorboard注意两个坑transformers 版本必须在 4.38.0 以上这个版本才开始原生支持 Gemma 架构。用旧版会直接报错提示不认识gemma这个模型类型。bitsandbytes 在 Windows 上兼容性差如果你坚持在 Windows 下跑遇到导入错误别慌去 GitHub 下载对应版本的预编译包手动安装。我自己更建议直接用 WSL2或者干脆用云 GPU省去一堆驱动麻烦。验证环境的代码也很简单import torch import transformers import peft print(torch.__version__) print(transformers.__version__) print(peft.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))torch.cuda.is_available()一定要输出True否则后面全是白搭。2.2 数据集格式设计别把 raw 数据直接喂给模型这是新手最容易踩的坑。大模型微调不是把你的一问一答直接塞进去而是要先把数据转换成模型能看懂的“对话模板”或者说“指令模板”。Gemma 的 instruct 版本用的是 ChatML 格式多轮对话长这样{ messages: [ {role: user, content: 你好请介绍一下你自己。}, {role: assistant, content: 我是 Gemma一个开源的语言模型。} ] }但如果是单轮指令微调业界更通用的格式是 Alpaca 风格{ instruction: 请用一句话概括下面的产品描述。, input: 这款手机搭载了5000mAh大电池支持120W快充重量仅有180克。, output: 一款主打续航和轻薄的快充手机。 }你要做的是写一个函数把这些结构化数据拼成模型真正看到的文本。我用的拼接方式是def format_example(example): if example.get(input, ).strip(): user_prompt ( ### Instruction:\n f{example[instruction]}\n\n f### Input:\n{example[input]}\n\n ### Response:\n ) else: user_prompt ( ### Instruction:\n f{example[instruction]}\n\n ### Response:\n ) return { text: user_prompt example[output] }拼接成一段完整文本后模型的输入就是一个 prompt输出就是你要的答案。这样做的道理其实很简单模型学的不是“对话”而是“看到某种输入就接某种输出”的语言习惯。你的 prompt 格式越固定模型就越容易学到规律。数据量方面我的经验是对于格式类微调500~2000 条高质量数据就能看到明显效果。数据量不是越多越好数据质量才是决定上限的因素。我见过有人拿 10000 条噪声很大的数据微调效果反而不如 800 条精心整理的。2.3 数据加载与切分细节数据文件我建议放在一个干净的目录下用 Hugging Face 的datasets库读取from datasets import load_dataset raw_dataset load_dataset(json, data_filestrain.jsonl) print(raw_dataset)JSONL 每行一个完整样本比大 JSON 数组更容易维护不用考虑格式化问题。如果数据量特别大比如好几万条可以额外加一步 shuffledataset raw_dataset[train].shuffle(seed42)数据切分我通常留出 5%~10% 作为验证集用于观察是否过拟合。注意别把所有数据都拿来训练后面你就知道验证集有多重要了。split dataset.train_test_split(test_size0.05, seed42) train_dataset split[train] eval_dataset split[test]3. 核心实现手把手跑通 Gemma 微调3.1 加载模型4bit 量化配置前面说过我用 QLoRA所以加载模型时要先配置量化器。这步最关键的是BitsAndBytesConfig的参数import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig ) bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_use_double_quantTrue, bnb_4bit_compute_dtypetorch.bfloat16 ) model_name google/gemma-2b-it model AutoModelForCausalLM.from_pretrained( model_name, quantization_configbnb_config, device_mapauto, torch_dtypetorch.bfloat16, ) tokenizer AutoTokenizer.from_pretrained(model_name)几个参数解释一下load_in_4bitTrue把模型权重量化到 4bit是省显存的根本手段。bnb_4bit_quant_typenf4NF4 是 bitsandbytes 优化的 4bit 数据类型比原始的 FP4 精度更好。QLoRA 论文里推荐的就是 NF4。bnb_4bit_use_double_quantTrue对量化常数再做一次量化能额外省一点显存大概 0.4GB / 7B 模型。bnb_4bit_compute_dtypetorch.bfloat16计算时用 bf16比 FP16 在数值稳定性上更好尤其在长序列训练时不容易溢出。这里有个很容易踩的坑device_mapauto让 transformers 自动把模型分配到各张卡上。如果你只有一张卡这么写没问题但如果多卡且显存不均偶尔会出现某些层被放到 CPU 的情况训练巨慢。单卡用户就放心用device_mapauto多卡用户建议手动指定CUDA_VISIBLE_DEVICES并配合max_memory参数。3.2 构造 LoRA 配置参数不是随便填的LoRA 配置的核心参数是r、alpha、target_modules。我在第一次跑的时候用的是默认值结果效果一般。后来调了一轮下面这组参数实测效果最好from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training model prepare_model_for_kbit_training(model) lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, biasnone, task_typeCAUSAL_LM, target_modules[ q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj, ] ) model get_peft_model(model, lora_config) model.print_trainable_parameters()这几个参数到底什么意思r秩决定 LoRA 补丁的规模。r16 表示每个被改的层里插入一个 16 维的低秩矩阵。r 越大可学习参数越多模型能学到的细节越多但也更容易过拟合。r8 适合小数据集r16 或 32 适合 2000 条以上的数据。lora_alpha缩放系数实际生效的缩放比例是alpha / r。比如 r16、alpha32实际缩放是 2 倍。alpha 调大相当于放大了 LoRA 补丁对模型的影响但不能过大否则训练会不稳定。target_modules目标模块要对哪些层做 LoRA。Gemma 的注意力层是q_proj/k_proj/v_proj/o_proj前馈层是gate_proj/up_proj/down_proj。全量加上给模型更大的自由度。有些人只改注意力层也能跑但效果通常差一些。跑完print_trainable_parameters()会输出类似下面的内容trainable params: 4,194,304 || all params: 2,616,413,696 || trainable%: 0.1603%看到 trainable% 只有 0.16% 是正常的LoRA 就是只训练极小一部分参数。这里插一句prepare_model_for_kbit_training这一步不能省。它会帮你做几件事把不需要训练的层设为 fp32防止量化导致梯度不稳、启用 gradient checkpointing省显存、把输入数据移动到正确的设备上。漏掉这步训练大概率会报错或者 loss 不下降。3.3 训练参数配置SFTTrainer 里每个参数都在回答“为什么”我这次用的是trl库里的SFTTrainer它比直接用transformers.Trainer省心得多——自动处理了数据拼接、padding 和 mask 逻辑。你只需要告诉它“我的数据集里哪一列是文本”。from trl import SFTTrainer from transformers import TrainingArguments training_args TrainingArguments( output_dir./gemma-lora-checkpoints, per_device_train_batch_size4, per_device_eval_batch_size4, gradient_accumulation_steps2, learning_rate2e-4, warmup_ratio0.05, lr_scheduler_typecosine, num_train_epochs3, evaluation_strategyepoch, save_strategyepoch, logging_steps10, save_total_limit2, load_best_model_at_endTrue, metric_for_best_modeleval_loss, fp16True, report_totensorboard, seed42, ) trainer SFTTrainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, tokenizertokenizer, max_seq_length1024, dataset_text_fieldtext, packingFalse, )我逐个讲下关键参数的选择逻辑per_device_train_batch_size4单卡每批 4 条样本。显存小的可以降到 2 或 1但不要通过狂降 batch size 来弥补一切——batch size 太小梯度噪声大训练不稳定。gradient_accumulation_steps2梯度累积 2 步再更新一次参数。效果等价于 batch size 8但显存占用不变。实际 batch size 4 × 2 8。learning_rate2e-4LoRA 的常用学习率区间是 1e-4 到 3e-4比全量微调的 5e-5 要大。因为可训练参数很少需要更大的步子才能学得动。如果 loss 震荡降到 1e-4 试试。warmup_ratio0.05前 5% 的步数内学习率从 0 慢慢升到峰值。防止一开始步子太大冲坏参数。lr_scheduler_typecosine学习率按余弦曲线衰减到 0。比起固定学习率余弦衰减在训练后期更稳不会在最优解附近来回震荡。fp16True混合精度训练训练速度翻倍且显存减半。注意如果你用的是bnb_4bit_compute_dtypetorch.bfloat16这里也可以用 BF16但许多卡不支持 BF16 加速FP16 是更通用的选择。max_seq_length1024用来控制输入序列长度上限。序列越长占显存越多。做格式微调 512 就够长文本场景再加大到 2048。3.4 训练过程的监控loss 曲线会说话训练开始后终端会刷出类似下面的日志{loss: 2.1234, learning_rate: 0.0002, epoch: 0.08} {loss: 1.3456, learning_rate: 0.0002, epoch: 0.16} {loss: 0.9876, learning_rate: 0.00019, epoch: 0.24}几个判断技巧起步 loss 在 2~3 属于正常。如果起步就差到 10 以上说明数据格式或 prompt 模板拼接有问题模型根本没看懂你想干嘛。loss 前几百步下降很快这是 Ada 系优化器的正常表现别急着高兴。训练到验证集 loss 开始回升、训练 loss 还在降就是过拟合了。此时应该早停或者减少 epoch。我用 tensorboard 可视化时观察到验证集 loss 在第二个 epoch 之后开始微涨训练集 loss 依然在掉果断把num_train_epochs调成 2最终效果最好。这就是前面预留验证集的价值。训练结束后适配器权重会保存在output_dir下你会看到adapter_model.safetensors和adapter_config.json两个文件。先别急着收工还有下面几步。4. 评估、合并与部署训练完不是终点4.1 效果验证先让模型说句话看看训练完成后第一件事不是评估指标而是直接加载模型手动喂几个测试样本看看输出长什么样。这比任何量化指标都直观。from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base_model AutoModelForCausalLM.from_pretrained( google/gemma-2b-it, device_mapauto, torch_dtypetorch.bfloat16, ) tokenizer AutoTokenizer.from_pretrained(google/gemma-2b-it) model PeftModel.from_pretrained(base_model, ./gemma-lora-checkpoints) prompt ### Instruction:\n请用一句话概括下面的产品描述。\n\n### Input:\n这款手机搭载了5000mAh大电池支持120W快充重量仅有180克。\n\n### Response:\n inputs tokenizer(prompt, return_tensorspt).to(cuda) outputs model.generate( **inputs, max_new_tokens128, do_sampleFalse, temperature0.7, ) response tokenizer.decode(outputs[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue) print(response)注意生成参数do_sampleFalse配temperature0.7是我惯用组合实际采样固定结果更可控如果你想看模型“发挥”可以do_sampleTrue但 temperature 不要超过 1.0否则容易胡言乱语。max_new_tokens128限制了生成长度太长会拖慢推理太短可能截断答案。肉眼看到的输出合格后再用测试集做一轮批量评测。对话/生成任务没有完美的自动指标我通常的做法是准备二十条测试样本让微调前后的模型各跑一遍对比输出质量人工打分。这方法土但有效。4.2 模型合并与权重保存微调产物是“补丁”adapter不能直接当成完整模型。推理时你需要先加载 base model再加上 adapter。这样能跑但部署环境每次都要多带一套加载逻辑。如果你要把模型转给其他人用或者部署到服务上建议把 LoRA 权重合并回主干导出一个完整模型merged_model model.merge_and_unload() merged_model.save_pretrained(./gemma-lora-merged, safe_serializationTrue) tokenizer.save_pretrained(./gemma-lora-merged)合并后的模型大小就是完整模型的大小2B 模型大概 5GBbf16比单独的 adapter约 16MB大得多但使用方便。实际部署时按需选择自己实验用 adapter 就够要交付就用合并后的。4.3 推理部署本地怎么跑、服务怎么开合并后的模型可以直接用AutoModelForCausalLM加载推理没有额外依赖。但如果要对很多用户提供服务原生 transformers 的推理效率偏低可以用 vLLM 加速pip install vllm vllm serve ./gemma-lora-merged --tensor-parallel-size 1 --max-model-len 2048vLLM 支持 OPENAI 兼容的 API 接口你本地起个服务后随便一个 HTTP 客户端就能调curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: ./gemma-lora-merged, messages: [{role: user, content: 你好请介绍一下自己}] }如果你没有 GPU 推理环境还可以考虑把合并后的模型用 llama.cpp 转成 GGUF 格式跑 CPU。但这一步对新手不太友好这里就不展开了。5. 实战中的坑与排查速查5.1 Hugging Face 下载相关异常别让网络卡住第一步新手最容易卡在这个环节——模型加载到一半报错或者卡住。如果你访问官方 Hub 不稳定有几个解法设置镜像端点。Hugging Face 官方提供了镜像站只需要设置一个环境变量HF_ENDPOINT就能加速下载。这个方法的使用非常普遍社区里也经常有人推荐。本地缓存。下载过一次的模型会缓存在~/.cache/huggingface/hub设置HF_HOME变量可以自定义缓存位置。第二次加载会直接读缓存不会再走网络。离线模式。如果目标机器完全没有外网可以把模型文件打包拷贝过去设置HF_HUB_OFFLINE1强制走本地缓存。顺带提一个很多人遇到的报错下载时出现 HTTP 418 或者 429 状态码。这两个状态码本质都是服务端的响应异常——429 是请求太频繁被限流418 是服务端拒绝服务状态码本身是彩蛋性质但实际表现就是下载失败。遇到这种情况等几分钟重试或者换非高峰时段下载比反复折腾本地配置更靠谱。5.2 显存不足与 OOM 应对训练中遇到最多的报错就是CUDA out of memory。如果你不想挨个试参数按这个顺序排查降低 batch size从 4 降到 2 再降到 1每次降 50%。打开/确认 gradient checkpointingprepare_model_for_kbit_training默认开启能省约 30% 显存。缩短 max_seq_length从 1024 降到 512显存占用立刻下降一大截。确认 4bit 量化生效有时候代码写错实际加载的是 8bit 甚至 16bit显存自然爆。显存占用大户除了模型权重还有激活值前向传播时中间变量。序列越长、batch 越大激活值占的显存越多。这也是为什么很多微调教程建议优先压序列长度而不是 batch size。5.3 loss 不降、loss 震荡、过拟合怎么判断loss 一直是 2.0 左右不动学习率太小调大到 5e-4 试试或者 prompt 模板和数据 format 函数对不上模型看到的输入是乱的。loss 上下剧烈震荡学习率太大降到 1e-4 或 5e-5或者 batch size 太大梯度方向不稳定。loss 降到很低但生成的文本驴唇不对马嘴这是数据问题。去看训练集的text字段有没有截断、有无残缺样本、是否有错误的占位符。数据的坑不会体现在 loss 上只会体现在结果上。5.4 细节坑pad token、seed 与 reproducibilitypad token 报错Gemma 的 tokenizer 默认没有 pad token但批量训练需要 padding。在SFTTrainer里会自动处理但如果你自己写数据 collator 就会报错。手动解决的方法是tokenizer.pad_token tokenizer.eos_tokenseed 的重要性训练前设置seed42transformers会自动固定大部分随机源但bitsandbytes的量化过程本身有随机性不同卡跑出来的结果可能有微小差异。想完全复现是不可能的但设置 seed 能保证“大致可复现”。别急着交结果每次训练完合并模型后务必用同一个 prompt 做回归测试防止改数据时无意破坏了原有能力。5.5 排查速查表为了方便以后快速定位问题我整理了一张速查表问题表现可能原因解决方案下载卡住/限流网络不畅、请求触发服务端限流设置镜像端点、离线缓存、错峰重试CUDA out of memorybatch/size太大、未走4bit量化降batch、降max_seq_length、检查量化生效loss 不降LR 太小、数据格式错误调大LR、检查 format 函数、打印样本文本loss 为 NaNFP16 溢出、LR 过大改用 BF16、降 LR加载模型报未知类型transformers 版本过低升级到 4.38生成结果空洞重复训练数据太少或过拟合增加数据多样性、减小 LoRA rank最后再分享几点个人体会跑通这一套流程之后我最大的感受是微调大模型没有想象中那么玄乎本质上就是一个“用数据训练小参数补丁”的过程。但每一步的细节都会影响最终效果尤其是数据格式的规范和 LoRA 参数的适配。我自己第一次跑的时候光在数据拼接上就折腾了大半天后来把所有样本打印出来肉眼检查才发现问题。如果你手里的任务也是“让模型学会某种固定格式的输出”或者“适配某个垂直场景的表达习惯”那这套 LoRA QLoRA 的流程已经足够应付大多数情况。先拿小模型跑通全流程再根据效果决定要不要换更大的底座模型——这个思路会比一上来就梭哈 7B/27B 稳得多。后续如果想把效果再提升一截可以试试在 LoRA 基础上叠加多轮对话数据的增量预训练或者用 PEFT 的其它方法比如 PiSSA、LoRA横向对比。模型微调这条路没有终点但跑通一次最小闭环之后后面再探索就不慌了。
返回列表