☰
LoRA微调实战:Llama与ChatGLM训练避坑指南
2026/10/8 8:36:08 网站建设 项目流程

简介:这份资源面向希望借助 LoRA 微调提升研发效率的算法工程师与 AI 应用开发者,围绕 Llama(Alpaca LoRA)与 ChatGLM(ChatGLM Tuning)两条技术路线展开,覆盖用户故事生成、测试代码生成、代码辅助生成、文本转 SQL、文本生成代码等典型研发场景,适合具备一定深度学习基础、想动手实践参数高效微调的中高级读者。压缩包共 93 个文件,约 53.46MB,以 jsonl 数据集、ipynb 训练笔记、md 说明文档、pdf 参考资料和 py 脚本为主,另含少量 json、csv、txt 及图片素材,兼顾数据、代码与文档三类用途。目前已有 442 人学习下载。资源按 alpaca、chatglm、datasets、codegen、text-to-code、text-to-sql、userstory 等模块组织,配套 merge-jsonl.py、text-to-code.py、text-to-sql.py 等脚本与训练日志,便于读者复现微调流程、理解数据构造方式,并迁移到自身业务场景中验证效果。

1. 从一张 24G 显卡说起:LoRA 微调到底在省什么

很多人第一次动 LoRA 的念头,都是被显存逼出来的。手里只有一张 24G 的卡,想拿 Llama 或 ChatGLM 做领域适配,全参数微调连权重带优化器状态直接爆掉,于是开始搜「lora微调是什么意思」「lora训练」这类词。LoRA 的思路很朴素:冻结原模型权重,只在注意力层的部分矩阵旁边挂一对低秩矩阵,训练时只更新这对小矩阵。它省的不是计算量,而是可训练参数量和优化器显存——通常能把可训练参数压到原模型的千分之一到百分之一量级。

这篇笔记讲的是自己动手训练 LoRA,覆盖两条主流路线:Llama 系(以 Alpaca LoRA 那套流程为代表)和 ChatGLM 系。适合手上有单卡或双卡、想跑通第一个领域 LoRA 的工程师,也适合已经跑过但 loss 不降、显存炸、合并后效果崩的人。下面从环境、数据、训练参数一路讲到合并与验证,中间会重点说清楚哪些参数是真有用的,哪些是玄学。

2. 训练环境与基座选型:Llama 和 ChatGLM 该先动哪个

2.1 两条路线的差异与选型理由

Llama 系和 ChatGLM 系在 LoRA 训练上的差别,主要不在 LoRA 本身,而在 tokenizer、注意力实现和对话模板。Llama 用 SentencePiece,中文场景下 token 效率偏低,同样一段中文,token 数往往比 ChatGLM 多出三到五成,直接后果是显存占用和训练时长都上去了。ChatGLM 原生中文 tokenizer,中文语料下更省,且它的对话格式([Round 1]\n\n问:...\n\n答:...)在社区工具里支持得比较成熟。

选型上我的建议是:语料以中文为主、显卡在 24G 以内,先动 ChatGLM;要做中英混合或后续想接生态里大量英文指令数据,选 Llama。Alpaca LoRA 那套脚本本质是给 Llama 系做的指令微调流程,它的价值在于把数据格式、训练循环、合并脚本串成了一条可复现的线,理解它之后再迁移到 ChatGLM 会快很多。

环境上,PyTorch 版本要和 CUDA 对齐,别在这上面省事。常见做法是用 conda 建独立环境,装torch、transformers、peft、datasets、accelerate这几个核心包。peft是 LoRA 的实现库,accelerate负责多卡和混合精度调度。版本之间耦合比较紧,transformers和peft尽量用同期发布的版本,否则容易出现target_modules名字对不上的问题。

2.2 环境搭建与依赖安装的可复现步骤

先建环境再装包,顺序别乱。下面这套命令在单卡 24G 机器上验证过,CUDA 12 系列。

# 建独立环境,Python 版本建议 3.10 conda create -n lora_train python=3.10 -y conda activate lora_train # 装 PyTorch,按自己 CUDA 版本选对应 index-url pip install torch==2.1.0 torchvision --index-url https://download.pytorch.org/whl/cu121 # 装训练相关依赖,版本尽量对齐 pip install transformers==4.36.0 peft==0.7.0 datasets==2.16.0 accelerate==0.25.0 pip install sentencepiece protobuf scipy

装完先验证 GPU 能不能被识别,别急着下模型。

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) # 期望输出:True 和你的显卡型号

torch.cuda.is_available()返回 False 的话,八成是 CUDA 版本和 PyTorch 编译版本不匹配,重装对应 wheel 即可,别去改系统驱动。transformers和peft的版本对应关系要留意:peft 0.7配transformers 4.36是稳的,跨大版本容易在LoraConfig的target_modules解析上报错。

2.3 基座模型下载与目录组织

模型权重建议单独放一个目录,训练脚本、数据、输出分开,后面排查问题会省很多事。

mkdir -p ~/lora_work/{models,data,output,scripts} # 以 ChatGLM3-6B 为例,权重放到 models 下 # 目录结构大致是: # ~/lora_work/models/chatglm3-6b/ # config.json tokenizer.model pytorch_model-*.bin

Llama 系同理,把基座权重放models/llama-7b/。权重文件格式上,现在社区越来越多用safetensors,加载更快也更安全,transformers直接支持,不用额外处理。目录组织清楚之后,训练脚本里所有路径都用绝对路径或基于工作目录的相对路径,避免在不同目录下跑脚本时找不到文件。

3. 数据准备:指令数据的格式与清洗

3.1 指令数据的标准结构

LoRA 微调的效果,七成看数据。指令微调的数据一般是「指令 + 输入 + 输出」三段式,Alpaca 格式就是典型:

[ { "instruction": "把下面这句话翻译成英文", "input": "今天天气不错", "output": "The weather is nice today." }, { "instruction": "给这段代码写注释", "input": "def add(a, b): return a + b", "output": "该函数接收两个参数并返回它们的和。" } ]

instruction是任务描述,input是可选上下文,output是期望回答。没有input的任务,把input留空字符串即可,别删字段,否则模板拼接时容易错位。ChatGLM 的对话格式略有不同,通常把多轮对话组织成prompt和response两列,训练时再套它的对话模板。

数据量上,领域适配起步 500 到 2000 条高质量样本就能看到明显变化,别一上来堆几万条低质数据。质量比数量重要得多,一条自相矛盾的样本能把模型带偏。

3.2 数据清洗与去重的实操

清洗主要做三件事:去重、去空、去超长。下面这段脚本处理 Alpaca 格式的 json。

import json from hashlib import md5 def clean_alpaca(path_in, path_out, max_len=2048): seen = set() kept = [] with open(path_in, "r", encoding="utf-8") as f: data = json.load(f) for item in data: ins = item.get("instruction", "").strip() out = item.get("output", "").strip() inp = item.get("input", "").strip() # 去空:指令或输出为空直接丢 if not ins or not out: continue # 去超长:按字符粗筛,真正长度以 tokenizer 为准 if len(ins) + len(inp) + len(out) > max_len: continue # 去重:用指令+输入做指纹 key = md5((ins + inp).encode("utf-8")).hexdigest() if key in seen: continue seen.add(key) kept.append({"instruction": ins, "input": inp, "output": out}) with open(path_out, "w", encoding="utf-8") as f: json.dump(kept, f, ensure_ascii=False, indent=2) print(f"原始 {len(data)} 条,清洗后 {len(kept)} 条") clean_alpaca("data/raw.json", "data/clean.json")

max_len这里按字符粗筛,真正判断要等 tokenizer 编码后看 token 数,因为中英文 token 比例差很多。去重指纹用「指令 + 输入」,不把输出算进去,是因为同一指令配不同输出往往意味着数据本身有冲突,这种应该人工看,而不是靠去重脚本一刀切。

3.3 数据集划分与 tokenizer 编码

清洗完要划分训练集和验证集,常见比例 9:1 或 95:5。验证集不是摆设,它是你判断有没有过拟合的唯一依据。

from datasets import Dataset from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained( "models/chatglm3-6b", trust_remote_code=True ) def format_sample(item): # 按基座对应的对话模板拼接,这里以通用指令模板示意 text = f"### 指令:\n{item['instruction']}\n" if item["input"]: text += f"### 输入:\n{item['input']}\n" text += f"### 回答:\n{item['output']}" return {"text": text} raw = Dataset.from_json("data/clean.json") raw = raw.map(format_sample) split = raw.train_test_split(test_size=0.1, seed=42) print(split)

trust_remote_code=True在加载 ChatGLM 这类带自定义代码的模型时必须加,否则会报找不到模型类。模板拼接要和推理时用的模板完全一致,训练用一套、推理用另一套,是新手最常见的翻车点之一,模型会答非所问。

4. LoRA 训练参数配置与启动

4.1 LoraConfig 的关键参数怎么设

LoRA 的核心参数就几个:r、lora_alpha、lora_dropout、target_modules。r是低秩矩阵的秩,越大表达能力越强但参数越多,常见 8 到 64,领域适配起步用 8 或 16 就够。lora_alpha是缩放系数,经验上设成r的两倍比较稳,比如r=8配alpha=16。lora_dropout防过拟合,小数据集上设 0.05 到 0.1。

target_modules是最容易出错的地方。Llama 系一般是q_proj、v_proj,有的实现会加上k_proj、o_proj。ChatGLM 的注意力层命名不同,常见是query_key_value。名字写错不会报错,但 LoRA 会挂到错误的层上,训练 loss 照降,效果却很差,这是典型的黑匣子式翻车。

from peft import LoraConfig, get_peft_model, TaskType lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, # 低秩维度,领域适配起步值 lora_alpha=16, # 缩放系数,通常为 r 的 2 倍 lora_dropout=0.05, # 小数据集防过拟合 target_modules=["query_key_value"], # ChatGLM 用这个;Llama 换成 q_proj/v_proj bias="none", # 不训练 bias,省显存 ) model = get_peft_model(base_model, lora_config) model.print_trainable_parameters() # 期望看到可训练参数占比在 0.1% ~ 1% 之间

print_trainable_parameters()这一步别省,它是你确认 LoRA 真的挂上去的唯一快速手段。如果可训练参数占比接近 100%,说明target_modules没匹配上,LoRA 没生效。

4.2 训练超参与 Trainer 配置

超参上,学习率是重灾区。LoRA 的学习率通常比全参微调大,常见 1e-4 到 3e-4,全参微调那套 2e-5 用在 LoRA 上会慢到怀疑人生。batch size 受显存限制,单卡 24G 跑 6B 模型,per_device_batch_size 设 1 到 4,再用梯度累积把等效 batch 拉到 16 到 64。

from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir="output/chatglm3-lora", per_device_train_batch_size=2, gradient_accumulation_steps=8, # 等效 batch = 2 * 8 = 16 learning_rate=2e-4, # LoRA 常用区间 num_train_epochs=3, logging_steps=10, save_strategy="epoch", evaluation_strategy="epoch", fp16=True, # 24G 卡开 fp16 省显存 warmup_ratio=0.03, lr_scheduler_type="cosine", report_to="none", ) trainer = Trainer( model=model, args=training_args, train_dataset=split["train"], eval_dataset=split["test"], ) trainer.train()

gradient_accumulation_steps和per_device_train_batch_size的乘积才是真正影响训练稳定性的等效 batch。等效 batch 太小,loss 会剧烈震荡;太大,收敛慢且可能过拟合。fp16=True在部分模型上会出现 loss 变 nan,遇到就换bf16=True,前提是显卡支持。warmup_ratio给 0.03 左右,让学习率从低往高爬,避免一开始就把预训练权重带偏。

4.3 启动训练与日志观察

启动后重点盯三样:loss 曲线、显存占用、验证集指标。训练 loss 平稳下降是基本要求,如果前几十步就掉到接近 0,多半是数据泄漏或标签错位。验证集 loss 先降后升,就是过拟合信号,该减 epoch 或加 dropout。

# 单卡直接跑 python scripts/train_lora.py # 多卡用 accelerate 启动 accelerate launch --num_processes=2 scripts/train_lora.py

显存占用上,6B 模型 LoRA 训练在 24G 卡上通常占 14G 到 20G,留点余量给激活值。如果 OOM,优先降per_device_train_batch_size,其次开梯度检查点(gradient_checkpointing=True),它会用时间换显存,训练慢一些但能跑起来。

5. 避坑与排查:训练 LoRA 最容易翻车的五件事

5.1 loss 不降或直接变 nan

现象:训练几十步后 loss 卡住不动,或者突然变成 nan。原因通常是学习率过高、fp16 数值溢出、数据里有空样本。解决:先把学习率降到 1e-4 试,再把fp16换成bf16,最后检查数据清洗有没有漏掉空 output。三者按顺序排查,别同时改,否则不知道是哪个起的作用。

5.2 合并后效果比训练时差很多

现象:训练时验证集表现正常,合并权重后推理答非所问。原因多半是训练和推理的对话模板不一致,或者合并时基座版本和训练时不是同一个。解决:把训练脚本里的模板函数和推理脚本里的模板函数抽成同一个文件共用,合并前核对基座权重的 hash。这个坑血泪经验最多,模板差一个换行都能让效果崩。

5.3 target_modules 写错导致 LoRA 没生效

现象:训练能跑,loss 也降,但效果和没微调差不多。原因:target_modules名字和模型实际层名不匹配,LoRA 挂到了不存在的层上,peft有时不报错。解决:加载模型后打印所有线性层的名字,确认目标层名。

for name, module in base_model.named_modules(): if isinstance(module, torch.nn.Linear): print(name) # 从输出里挑出注意力层的真实名字,填进 target_modules

5.4 显存够但训练极慢

现象:显存没满,但每步耗时远超预期。原因:数据加载没开多进程、序列长度设得过长、或者没开混合精度。解决:dataloader_num_workers设 2 到 4,max_length按数据实际分布设,别一律 2048,大部分样本可能只有几百 token,padding 到 2048 纯属浪费。

5.5 验证集 loss 上升但训练 loss 还在降

现象:训练 loss 一路降,验证 loss 中途反弹。原因:过拟合,数据量小或 epoch 太多。解决:减 epoch、加lora_dropout、加数据。别指望靠调学习率救过拟合,根子在数据量和训练轮数上。小数据集上 2 到 3 个 epoch 通常就够,再多就是让模型背答案。

6. 合并权重与效果验证:把 LoRA 真正用起来

训练完的 LoRA 权重是独立的小文件,推理时可以直接加载,也可以合并进基座。直接加载适合快速验证,合并适合部署。合并脚本如下:

from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base = AutoModelForCausalLM.from_pretrained( "models/chatglm3-6b", trust_remote_code=True, device_map="auto" ) model = PeftModel.from_pretrained(base, "output/chatglm3-lora") # 合并并保存 merged = model.merge_and_unload() merged.save_pretrained("output/chatglm3-merged", safe_serialization=True) tokenizer = AutoTokenizer.from_pretrained( "models/chatglm3-6b", trust_remote_code=True ) tokenizer.save_pretrained("output/chatglm3-merged")

merge_and_unload()把 LoRA 权重加回基座并卸载适配器,safe_serialization=True存成safetensors格式。合并后一定要做对比验证,别合并完就直接上线。

验证方法上,我一般准备一组固定测试集,分别用「基座」「基座 + LoRA」「合并后模型」跑同一批问题,人工对比。重点看三类:领域内问题是否答得更准、通用问题是否退化、格式是否稳定。领域 LoRA 常见的副作用是通用能力下降,如果退化明显,说明训练数据太单一或学习率过高。

进阶一点的做法是控制变量做消融:固定数据,只改r(8 / 16 / 32)各训一版,看验证集指标和显存占用的权衡。多数领域任务上r=16是性价比拐点,再往上收益递减而显存和过拟合风险都涨。另一个技巧是分层学习率,注意力层给正常学习率,其他层给更小的,但这个在 LoRA 上收益有限,因为可训练参数本来就少,不建议新手一上来就折腾。

我自己踩过最深的坑,是第一次训完直接合并上线,结果线上效果和验证集对不上,回头查了两天才发现推理脚本的模板比训练时少了一个换行。从那以后我养成一个习惯:训练脚本和推理脚本共用同一个build_prompt函数,改一处两边都变。这个习惯比任何调参技巧都值钱。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询