简介:面向AI研发工程师与算法学习者,这份以LoRA微调实战为核心的学习资料包,聚焦Llama(Alpaca LoRA)与ChatGLM(ChatGLM Tuning)两大主流模型的低秩适配训练。包内围绕用户故事生成、测试代码生成、代码辅助生成、文本转SQL、文本生成代码等真实研发提效场景,提供了完整实验流程与实现参考,覆盖从数据准备、模型微调到结果验证的各个环节,帮助读者理解LoRA原理并快速上手自定义微调任务,包括关键超参设置、数据格式构造与训练效果评估。
资源包共93个文件,总大小约53.46MB,包含20个jsonl训练数据文件、18个ipynb交互式笔记本、11个md说明文档、8个py自动化脚本,以及pdf、txt、csv等辅助材料。其中ipynb文件对应alpaca-lora与chatglm-tuning的逐步实现过程,py脚本涵盖数据合并、文本转代码等工具,jsonl提供用户故事、测试代码、SQL生成等场景的样本数据,md与pdf则记录了实验笔记、原理讲解与目录说明,整体结构清晰,适合按模块学习或直接复用,也便于二次开发。
目前已有442人浏览学习这份资源包,适合对大模型微调有一定基础、希望借助完整案例动手实践LoRA训练并提升AI研发效能的读者,无论是学习研究还是工程落地,都能从中获得直接可参考的实战经验。
1. LoRA 自训为什么是 AI 研发提效的最短路径
很多团队在引入大模型时卡在同一个地方:跑通推理很容易,但想让模型贴合自己的业务数据,微调这一步要么没卡跑不动,要么外包出去既贵又怕数据泄露。自己动手训练 LoRA,本质上是用参数高效微调把这件事的成本降下来——不碰全部权重,只训练一小批低秩矩阵,让 LoRA 适配器学会业务指令,Llama 和 ChatGLM 这类开源基座都能用这套思路改造。实际做下来,一张消费级显卡就能微调 7B 量级的模型,从准备数据到拿到可用权重,一个工作日足够。适合已经会用开源模型做推理、想在内部数据上做定制化的研发团队,也适合作为团队里的一个通用提效工具沉淀下来。
2. 动手前先定三件事:基座模型、数据格式与训练框架
LoRA 训练的门槛不在代码,而在前置选择。基座模型决定了你要处理什么 tokenizer、什么对话格式;数据格式决定了模型能不能学会指令跟随;训练框架决定了显存占用和出问题的排查难度。这三件事不先定清楚,训练脚本写出来也很难直接跑通。
2.1 基座模型怎么选:Llama 系与 ChatGLM 系的差异
我一般会根据业务语言倾向先定基座。Llama 系(中文场景下一般用它的中文增量预训练版本)对英文和代码类指令表现稳定,tokenizer 只认文本结构,对话格式完全靠 prompt 模板控制,灵活性高;ChatGLM 系自带中文对话的分词器和相对固定的对话组织方式,中文指令跟随的默认表现更好,但灵活性略低。
两者在训练侧有几个实际差别。Llama 系的 tokenizer 没有 pad token,加载后必须手工指定,否则批训练时数据打包会报错或损失计算异常;ChatGLM 系自带 pad token,少一个坑。Llama 系是标准因果语言模型,用TaskType.CAUSAL_LM;ChatGLM 系虽然结构上是前缀语言模型,但在主流 LoRA 训练实践中,同样可以通过 peft 的CAUSAL_LM任务类型跑通,这不影响 LoRA 适配器的正确性。
| 对照项 | Llama 系 | ChatGLM 系 |
|---|---|---|
| 中英文倾向 | 英文/代码较强,中文需选中文基座 | 中文默认表现好 |
| pad token | 无,需手动设置 | 自带 |
| 对话格式 | prompt 模板控制,自由度高 | 结构化角色对话,固定性好 |
| 常见任务类型 | CAUSAL_LM | CAUSAL_LM(实践中通用) |
选基座时我有个习惯:先拿模型跑一次你业务里最典型的 20 条提示词,看原始输出有多离谱。如果基座本身连基本指令都听不懂,LoRA 是救不回来的,LoRA 做的是调整风格、格式和领域知识,补不了基座的基础能力。
2.2 数据格式:Alpaca 格式与 ChatGLM 格式怎么组织
数据格式是整个训练里最值得花时间的部分。LoRA 微调的效果上限由数据质量决定,这一点被很多人忽略。当前最常见的做法是用 Alpaca 式指令数据格式,每条样本包含指令、可选输入和期望输出,保存成 JSON 或 JSONL。
Llama 系和 ChatGLM 系在 peft + transformers 训练路径下都能用这套格式,但组织要略作调整。Llama 系单轮指令用instruction + input + output三字段就够;ChatGLM 系如果要保留多轮关联,需要把历史对话合并进输入字段。
我的建议是,第一版数据只做单轮指令,不要上来就挑战多轮。多轮对话的构造方式不同训练脚本差异很大,容易排查不清。先拿 50 条高质量单轮样本跑通链路,再扩展多轮。
数据文件参考结构:
data/train.json # 训练集 data/valid.json # 验证集,建议至少 20 条训练集每条样本的 JSON 结构:
{ "instruction": "把下面这句话翻译成英文:", "input": "今天天气很好。", "output": "The weather is nice today." }单轮样本构造时我一般会注意三点:一是instruction要写清楚任务边界,不依赖示例猜测;二是input可空,但空字段建议保留而不是删除;三是全量样本覆盖业务场景要均匀,别让某类指令占 80%,模型学出来会偏。
2.3 训练框架与显存预算
训练框架我推荐 peft + transformers + accelerate 的组合。peft 负责注入 LoRA 适配器,transformers 负责加载模型与训练循环,accelerate 负责多卡和混合精度。三者都是当前主流方案,遇到问题检索起来容易找到参考。
显存预算直接决定你能不能在本机跑。7B 量级的模型,加载半精度权重大约占 14GB 显存,纯 LoRA 训练的优化器状态和激活值额外占用不多,16GB 显存的消费级显卡基本可以跑;13B 量级半精度加载就要 26GB,单卡 24GB 会很紧张,通常要配合 8bit 量化加载。训练脚本里可以加上load_in_8bit=True来压显存,但要留意它会影响训练速度,显卡能装下时我更倾向半精度直接跑。
3. 自己动手训练:Llama 与 ChatGLM 两套 LoRA 脚本
下面两套脚本是我在实际项目中常用的训练路径,一套面向 Llama 系,一套面向 ChatGLM 系。两者的核心差异在 tokenizer 处理和模型加载参数上,LoRA 配置基本一致。脚本在单卡、16GB 显存环境下验证可用,数据量 500 条、训练 3 个 epoch 大约 30~60 分钟。
3.1 Llama 系:从加载到训练的完整脚本
import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForSeq2Seq ) from peft import LoraConfig, get_peft_model, TaskType from datasets import load_dataset model_path = "/data/models/llama-7b" # 换成你的基座模型路径 data_path = "./data/train.json" # 加载 tokenizer,并手动补上 pad token tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token # 关键:Llama 系无 pad token # 加载模型:半精度 + 单卡 model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True ) # 注入 LoRA 适配器 lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, lora_alpha=16, lora_dropout=0.05, target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 确认可训练参数占比 # 构造指令数据 def format_sample(sample): text = f"""[INST] {sample['instruction']} {sample['input']} [/INST] {sample['output']}""" return {"text": text} def tokenize_fn(sample): result = tokenizer(sample["text"], truncation=True, max_length=512) result["labels"] = result["input_ids"].copy() return result dataset = load_dataset("json", data_files=data_path)["train"] dataset = dataset.map(format_sample).map(tokenize_fn, remove_columns=dataset.column_names) # 训练参数 training_args = TrainingArguments( output_dir="./lora-llama", per_device_train_batch_size=2, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_strategy="epoch", fp16=True, remove_unused_columns=False, ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, data_collator=DataCollatorForSeq2Seq(tokenizer), ) trainer.train()tokenizer.pad_token = tokenizer.eos_token这行是所有 Llama 系 LoRA 训练里最容易被漏掉的设置。少了它,批处理时 pad 位置可能填成未知 token,训练进程本身可能不报错,但损失和生成质量都会变得奇怪。target_modules指定注入 LoRA 的线性层,q/k/v/o 四个投影层是常见做法,覆盖注意力计算的主要矩阵;r=8是低秩矩阵的秩,lora_alpha=16是缩放系数,两者配合决定适配器容量。fp16=True开启混合精度,显存占用和训练速度都能受益。
3.2 ChatGLM 系:脚本差异与三个注意点
import torch from transformers import ( AutoModel, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForSeq2Seq ) from peft import LoraConfig, get_peft_model, TaskType from datasets import load_dataset model_path = "/data/models/chatglm-6b" # 换成你的基座模型路径 data_path = "./data/train.json" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) # ChatGLM 加载时建议加 dtype 参数,避免默认 float32 显存爆炸 model = AutoModel.from_pretrained( model_path, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True ) lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, lora_alpha=16, lora_dropout=0.05, target_modules=["q", "v"], ) model = get_peft_model(model, lora_config) def format_sample(sample): prompt = sample["instruction"] if sample.get("input"): prompt += sample["input"] return {"prompt": prompt, "response": sample["output"]} def tokenize_fn(sample): # 用固定分隔符连接 prompt 和 response,response 部分参与损失计算 prompt_text = sample["prompt"] response_text = sample["response"] full_text = prompt_text + "[SEP]" + response_text encoded = tokenizer(full_text, truncation=True, max_length=512) labels = encoded["input_ids"].copy() # 只对 response 部分计算损失,避免模型只学会复读 prompt prompt_encoded = tokenizer(prompt_text + "[SEP]") prompt_len = len(prompt_encoded["input_ids"]) labels[:prompt_len] = -100 encoded["labels"] = labels return encoded dataset = load_dataset("json", data_files=data_path)["train"] dataset = dataset.map(format_sample).map(tokenize_fn, remove_columns=dataset.column_names) training_args = TrainingArguments( output_dir="./lora-chatglm", per_device_train_batch_size=2, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_strategy="epoch", fp16=True, remove_unused_columns=False, ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, data_collator=DataCollatorForSeq2Seq(tokenizer), ) trainer.train()第一个注意点:ChatGLM 系target_modules用["q", "v"]还是["query", "value"],取决于模型权重里的实际命名。加载模型后先打印model.state_dict().keys()确认,不要照抄网上配置,权重命名不同会导致适配器没有注入到任何模块,训练完模型根本没变化。第二个注意点:trust_remote_code=True必须保留,这类模型的实现依赖仓库里的自定义代码,去掉它加载阶段就会失败。第三个注意点:构造labels时把 prompt 部分置为-100,这个做法比直接让模型对全序列计算损失更合理,能让模型专注学习怎么回答,而不是背下提问方式。
3.3 启动命令与训练日志观察
脚本保存成train_lora.py后,启动命令保持简单:
python train_lora.py显存不够时先看是不是其他进程占卡了,用nvidia-smi确认。日志里主要盯着loss的变化趋势,正常情况第一个 epoch 内 loss 会明显下降,如果三个 step 过去 loss 纹丝不动,先停掉检查学习率和数据格式,不要盲目加长时间。
4. LoRA 关键参数:rank、alpha、学习率与数据长度怎么调
LoRA 训练参数少,但每个都直接决定最终效果。我见过不少项目把默认参数一跑了事,效果不对就怀疑数据,其实多半是参数不合适。这一章把几个必调参数讲透。
4.1 rank 与 alpha:适配器容量与训练稳定性的平衡
r是低秩矩阵的秩,可以理解为 LoRA 适配器的可学习容量。r=8处理常见指令跟随和风格迁移足够;领域数据量大且差异明显时提到16;r=64这种大秩在部分中文指令微调里出现过效果更好的案例,但随之而来的是过拟合风险和训练时间上涨。lora_alpha是缩放系数,常规设置为alpha = 2 * r,即r=8时alpha=16。这个比例给出的适配权重变化幅度在多数任务里表现稳定,alpha调大相当于加大 LoRA 对输出的影响,但过大可能破坏基座原有能力。
| 参数 | 常见范围 | 调参方向 |
|---|---|---|
| r | 8 / 16 / 32 | 数据量大、任务难时调大 |
| lora_alpha | 16 / 32 | 通常取 2 倍 r,看输出变化幅度微调 |
| lora_dropout | 0.0 / 0.05 / 0.1 | 数据量小的时候调大防过拟合 |
| target_modules | q/v 或 q/k/v/o | 按模型权重实际命名确认 |
lora_dropout容易被忽略。数据量只有几百条时,0.05的 dropout 能明显降低过拟合;数据量几千条以上时调回0.0也可以,性能影响不大。判断依据很简单:训练 loss 降得很低但验证集表现差,先加 dropout。
4.2 学习率、batch 与 epoch:LoRA 与全参微调的参数差异
LoRA 的学习率设置跟全参微调有显著差异。全参微调常用5e-5到1e-4,而 LoRA 只训练少量新增参数,学习率可以放开一个量级,2e-4到3e-4是常见区间。学习率过低时适配器几乎学不到东西,表现是 loss 缓慢下降但生成文本跟基座没区别;学习率超过5e-4则容易震荡,loss 曲线上下跳动,后期生成质量不稳。
batch 的策略受显存约束。per_device_train_batch_size在 16GB 显存、7B 模型下通常只能设到 2 或 4,靠gradient_accumulation_steps凑等效 batch size。我一般把两者乘积控制在 16 到 32 之间,等效 batch 过大在小数据量上反而容易欠拟合。epoch 方面,几百条数据 3 个 epoch 通常够用,1 个 epoch 效果往往不充分,超过 5 个 epoch 过拟合风险急剧上升。验证方法是看训练结束后模型在验证集上的输出是否出现复读、空答。
4.3 max_length 与 padding:损失被稀释的隐形开关
max_length直接决定训练样本截断长度,也影响显存占用。设置太短,长指令的关键信息被截掉;设置太长,短样本被迫填充大量 pad token,模型会在这些位置计算无意义损失。常见做法是统计训练集中样本长度分布,取 90 分位长度作为max_length,这样既保住长样本,又不让短样本填充太多。
损失稀释问题在训练日志里看不到,但最终效果会暴露。模型输出变得短而敷衍时,除了检查数据,还要确认 padding 部分的标签是否被正确忽略。上面脚本里我用DataCollatorForSeq2Seq动态 padding,它不会把 pad 位置纳入损失计算,但如果自己写了数据整理函数,很容易把 pad token 的标签也留下,导致模型花大量精力学习预测无意义的 pad token。排查方式是把训练样本里labels中-100之外的 token 数量统计出来,对比总长度,如果有效标签占比过低,就要调整 max_length 或数据截断策略。
5. LoRA 训练避坑:五条亲测踩坑记录
LoRA 训练表面简单,实际踩坑点集中且隐蔽。下面五条都是我在不同项目里真实遇到过的,按「现象 → 原因 → 解决」的方式整理,新手照着排查能省大半天时间。
5.1 loss 在下降,生成结果却像在复读输入
现象:训练日志里 loss 正常下降,但推理时模型把用户输入原样重复一遍,或者输出跟输入高度相似。
原因:数据构造时没有把输入部分排除在损失计算之外。模型把「阅读输入并输出」学成了「完整复述输入」,因为在它看来预测下一个 token 的最稳妥策略就是复读。很多脚本只构造了input_ids而忘记构造labels,或者labels直接拷贝了全部input_ids,导致前面提到的 prompt 部分也参与损失计算。解决:手动构造labels,把 prompt 部分置为-100,只让 response 部分计算损失。
5.2 显存明明够,训练却报 OOM
现象:模型加载成功,但训练循环一开始就报 CUDA out of memory。
原因:remove_unused_columns=False只解决了数据列问题,但激活值显存峰值发生在反向传播阶段。batch size 设 2 能加载,设 4 可能就爆。另外,device_map="auto"在多卡机器上会把模型切到多张卡,单卡显存看起来空闲,但训练时数据传输和激活值计算可能集中在某一侧。
解决:先把per_device_train_batch_size降到 1,验证能跑后再逐步加回;检查是不是多卡机器上device_map="auto"把模型分散了,单卡训练时去掉这个参数;实在压不下来,再加上load_in_8bit=True或load_in_4bit=True,用量化换显存。
5.3 训练完的 adapter 加载到另一台机器上效果完全不对
现象:同一份 adapter 文件,在训练机器上推理效果不错,拷到部署机器上重新加载基座模型后,输出变得不可用。
原因:最常见的是精度不一致。训练时基座模型用float16或 8bit 加载,部署机器用float32加载,LoRA 适配器的数值是在原精度下学出来的,换精度后权重激活范围变化,适配信号失真。其次是基座模型权重版本不一致,基座权重差一个版本,adapter 全部失效。
解决:部署时严格保持与训练一致的torch_dtype;记录训练所用的基座版本,部署前校验模型目录哈希;有条件的话,部署时直接加载合并后的完整权重,绕开适配器与基座版本耦合问题。
5.4 数据量几千条,反复训练但仍学不到业务规则
现象:训练集覆盖了某类业务规则,loss 也降了,但推理时模型对这类问题答非所问。
原因:数据格式里instruction和input混在一起,或者instruction本身写得不规范,同一类任务表述千差万别,模型没有学到统一模式。更隐蔽的原因是数据里正负样本不均衡,某类指令下只有正例没有反例,模型学不到边界。
解决:先做数据清洗,把同类指令的表述统一成固定模板;检查样本分布,每条 instruction 对应的输出确保覆盖不同正确的表达;再做一次「链路验证」,用 20 条数据训练跑通,确认这个数据能学出可用结果,再扩大数据量。数据质量问题是 LoRA 训练里最花时间的部分,没有捷径。
5.5 训练日志显示 loss 数值很低,但模型实际输出乱码
现象:loss 从高位降到接近 0,推理时模型输出却是乱码或重复标点。
原因:labels构造错误导致模型学习到了错误的注意力模式,常见是 tokenizer 返回的attention_mask与input_ids长度不一致,或在拼接[SEP]等分隔符时没有同步处理 labels。更常见的是 pad token 在 labels 里没有被忽略,模型学会了预测 pad token 本身。
解决:检查 tokenizer 编码后的input_ids、attention_mask、labels三者长度,用脚本打印一条样本确认-100的位置分布;打印trainer中实际输入 batch 的形状,确认 padding 后各字段同步。乱码问题九成出在数据整理函数里,不要先怀疑模型和框架。
6. 训练完还不够:LoRA 合并、权重校验与部署验证
模型训练完成只算走了一半。最后一个阶段要回答三个问题:这个 LoRA 到底学到了什么、部署时用哪种方式加载、有没有可复现的验证流程。我的做法分三步走。
第一步是做「过拟合链路自检」。正式训练前拿 20 条样本训 1 个 epoch,目标不是效果而是确认链路通。如果 20 条数据训练后模型能在这些样本上复现出接近原文的输出,说明数据构造、损失计算、适配器注入全链路正常。这一步能过滤掉大部分脚本问题,避免拿几百条数据训了半天才发现基础 bug。
第二步是选择部署加载方式。LoRA 训练产物一般是 adapter 权重目录,包含adapter_config.json和adapter_model.bin。部署时有两种常见做法:
# 方式一:基座 + adapter 组合加载(推荐,保留可迭代性) from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model = AutoModelForCausalLM.from_pretrained( base_model_path, torch_dtype=torch.float16, device_map="auto", ) model = PeftModel.from_pretrained(base_model, "./lora-llama/checkpoint-3") # 方式二:合并权重,生成一个完整的推理模型 merged_model = model.merge_and_unload() merged_model.save_pretrained("./merged-model") tokenizer.save_pretrained("./merged-model")方式一保留了 adapter 单独存储的特性,业务迭代时只需要重新训练 adapter,不用动基座权重;方式二更适合部署到生产环境,推理框架加载完整模型少一层依赖,也不存在 adapter 与基座版本错配的问题。我一般两个都保留:迭代阶段用方式一,上线用方式二。
第三步是效果验证。不要只看 loss,要拿 10~20 条训练集外的问题逐条看输出,关注三个点:格式是否遵循要求、领域术语是否准确、模型是否还在回答与任务无关的内容。如果大部分样本通过,再进入团队试用。
我自己在这条路上踩过最大的坑就是赶进度跳过链路自检,结果花两天时间在错误脚本上反复调参。现在养成的习惯是任何数据变更后都先跑 20 条过拟合实验再放量训练,看起来多花了十几分钟,实际省下的是以小时计的返工时间。这套方法适配器和脚本一旦沉淀下来,换数据、换基座都能复用,希望帮到你。
本文还有配套的精品资源,点击获取