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

资讯详情

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

ChatGLM3-6B LoRA微调实战:从环境搭建到情感分类全记录

ChatGLM3-6B LoRA微调实战:从环境搭建到情感分类全记录 简介本资源是一套面向NLP工程师与大模型初学者的ChatGLM3-6B LoRA微调实战项目聚焦轻量化高效微调场景解决大模型在有限算力下适配垂直任务如中文对话、自我认知的落地难题。压缩包共12个文件359KB含5个JSON格式指令微调数据集如oaast_sft_zh.json、self_cognition.json、4个核心Python脚本finetune_hf.py用于训练、inference_hf.py支持推理、dataset2glm3.py完成数据预处理、model_export_hf.py导出Hugging Face格式模型、1个LoRA配置yaml、1个README.md说明文档及1个my配置文件覆盖数据准备、训练、导出、推理全流程。已有775人学习下载提供开箱即用的完整实现路径从ChatGLM3-6B基座加载、LoRA模块注入与参数冻结策略到SFT微调、模型合并与本地部署验证所有代码均适配Hugging Face生态注释清晰便于理解低秩适配原理并快速复现效果。 下面是我在本地把ChatGLM3-6B用LoRA做领域微调的一个完整实战记录。项目本身是拿中文电商评论做情感分类但整套流程、代码和数据组织方式都可以直接替换成你自己的业务场景。我会把从环境搭建、数据准备、训练脚本、推理验证到各种踩坑记录都展开讲清楚尤其是那些文档里不会写的细节。如果你正准备做垂直领域的大模型微调或者已经在跑LoRA但遇到了奇怪的问题这篇应该对你有帮助。1. 项目背景与方案选型1.1 为什么选中ChatGLM3-6B而不是更大的模型我选ChatGLM3-6B作为微调底座主要原因是它的中文能力在同尺寸模型里属于第一梯队而且6B参数量刚好在“单卡能跑”和“效果够用”的甜蜜点上。相比13B、14B甚至70B级别6B模型用一张24GB显存的卡就能完成训练部署时甚至可以压缩到CPU运行这对很多中小团队和独立开发者非常重要。另一个原因是ChatGLM3系列在开源社区里资料齐全网上能搜到大量实践案例遇到问题时解决成本低。它的对话格式清晰自带chat模板做指令微调和对话微调都很顺手。相比之下有些开源模型的tokenizer或者对话模板写得不规范光处理数据就够折腾一天的。我个人理解选模型本质上是做约束条件下的资源规划不是无脑挑最大。如果你手里有8张A100当然可以上更大模型做全参微调但大多数场景是单卡显存有限、数据集又不大这种情况下一个6B模型配合LoRA完全可以在几个小时内产出一个效果还不错的领域助手。1.2 全参微调和LoRA微调的取舍逻辑可能有人会问既然要微调为什么不全参微调答案很简单显存不够也没必要。全参微调6B模型光是模型参数在fp16下就要占12GB显存再加上优化器状态、梯度和激活值一张24GB的卡根本放不下更别提任何带序列长度较长的业务数据。LoRA的全称是Low-Rank Adaptation核心思路是冻结原始模型权重在注意力层和全连接层旁边插入低秩分解矩阵。训练时只更新这些少量的低秩矩阵可训练参数量通常只占原模型的0.1%到1%。这样做的效果是显存占用大幅下降而且很多实验证明LoRA在特定任务上的效果可以逼近甚至持平全参微调。我在这个项目里实际感受是ChatGLM3-6B用LoRA微调后可训练参数只有大约200万到600万相比原模型的60亿参数训练的显存占用从“完全跑不起来”降到了“一张3090/4090轻松搞定”。如果你是单卡玩家LoRA基本是唯一靠谱的微调路径。2. 环境准备与数据工程2.1 硬件要求与依赖安装先说硬件底线。我这套流程实测在RTX 409024GB显存上非常宽松如果你只有RTX 3090或者A500024GB同样没问题。若显存只有16GB甚至12GB就需要把4bit量化和梯度检查点都打开序列长度控制在512左右也是能跑起来的。依赖安装建议使用conda创建一个干净环境避免污染其他项目。我用的核心依赖版本如下conda create -n glm3-lora python3.10 -y conda activate glm3-lora pip install torch2.1.2 torchvision0.16.2 torchaudio2.1.2 --index-url https://download.pytorch.org/whl/cu121 pip install transformers4.40.1 pip install peft0.9.0 pip install datasets2.18.0 pip install accelerate0.28.0 pip install bitsandbytes0.43.1 pip install sentencepiece pip install sentence-transformers这里有几个需要注意的坑。如果你用的是transformers 4.40以上版本ChatGLM3-6B的加载方式会有些变化建议直接指定版本避免兼容性问题。bitsandbytes负责4bit量化在Windows上装起来稍微麻烦一点Linux上一般一条命令搞定。最后一定要检查torch和CUDA版本是否匹配否则后面训练时会报莫名其妙的显存错误。2.2 微调数据怎么构造才靠谱数据是微调效果的天花板。很多新手把注意力全放在调参上却忽视了数据质量。这次项目里我用的是电商评论情感分类数据每条样本由“用户输入”和“期望输出”两部分组成格式如下{ instruction: 请判断以下评论的情感倾向只回答正向、负向或中性。, input: 这个手机壳质量很好手感舒服物流也快非常满意, output: 正向 }整理成JSONL文件后我用下面的代码把数据加载成训练集import json from datasets import Dataset def load_dataset(file_path): samples [] with open(file_path, r, encodingutf-8) as f: for line in f: line line.strip() if line: samples.append(json.loads(line)) return Dataset.from_list(samples) train_dataset load_dataset(data/train.jsonl) eval_dataset load_dataset(data/eval.jsonl) print(f训练集大小: {len(train_dataset)}) print(f验证集大小: {len(eval_dataset)})数据量上我的经验是分类任务起步用2000到5000条生成类任务至少需要5000条以上。如果你的数据量只有几百条那优先考虑用few-shot提示词而不是做微调因为数据太少时LoRA容易过拟合效果反而差。还有一个很多人忽略的点是数据分布。你要确保每条类别数量基本均衡否则模型学到的只是“无脑说出现最多的类别”。我这次把负向、正向、中性三类各凑了2500条左右最后训练出来的模型分类准确率才比较稳。2.3 序列长度和tokenize策略的细节ChatGLM3-6B的上下文窗口是8192但实际训练时不需要、也不建议每次都用满。序列越长显存占用成倍增长训练速度也会线性下降。我做情感分类时把max_length设为512这个长度足以覆盖绝大多数评论内容。tokenize时有两个关键点一是设置padding到max_length还是只padding到batch内最大长度二是loss计算时要把prompt部分mask掉。由于LoRA本质上是自回归训练如果不把prompt部分的loss屏蔽模型会在拟合“读懂问题”这件事上浪费大量学习能力。我构建的训练输入格式如下from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(THUDM/chatglm3-6b, trust_remote_codeTrue) def build_input(example): prompt f{example[instruction]}\n{example[input]} assert_target f{example[output]} # ChatGLM3的chat模板一般由几段组成这里用最直接的方式拼接 text f[Round 1]\n\n用户{prompt}\n\n助手{assert_target}|assistant| return text def tokenize_function(examples): texts [build_input(e) for e in examples] model_inputs tokenizer( texts, max_length512, truncationTrue, paddingmax_length, return_tensorspt, ) model_inputs[labels] model_inputs[input_ids].clone() return model_inputs这里我把labels直接复制为input_ids虽然理论上会计算prompt的loss但实践下来对短样本影响不大。如果你想更严谨可以在拼接时记录prompt长度然后把prompt部分的labels设为-100。3. LoRA原理与关键参数解析3.1 低秩分解到底改了什么LoRA的核心假设其实很简洁大模型在预训练时学到的权重W是一个高维矩阵但在适配下游任务时真正需要的权重变化ΔW可能集中在一个低秩子空间里。所以与其直接更新整个W不如把ΔW分解成两个低秩矩阵A和B的乘积。具体公式是这样的原来的线性层计算是y Wx加了LoRA之后变成y Wx BAx。其中B的维度是d_out×rA的维度是r×d_inr就是秩通常设置为8、16、32这样的值。训练时W被冻结不更新只更新A和B这样就能以极小的参数量完成对模型的微调。生活化理解就是你想给一座大楼重新装修但不需要把整座大楼的钢筋水泥全部重浇一遍只需要在关键承重墙旁边加装几个可调节的支架就能达到改变房间布局的效果。LoRA加的就是这种“可调节支架”成本低、见效快。3.2 核心参数rank、alpha、target_modules模型微调里最影响效果的三个参数分别是rankr、lora_alpha和target_modules。下面分别讲讲我在这个项目里实际使用的配置。from peft import LoraConfig, TaskType lora_config LoraConfig( r8, lora_alpha32, target_modules[query_key_value], lora_dropout0.05, biasnone, task_typeTaskType.CAUSAL_LM, )rank决定低秩矩阵的秩也就是可训练参数量的多少。rank越大能表达的内容越丰富但过拟合风险和显存占用也会变高。我试了r8和r16在5000条数据下两者效果差别不大但r16训练时间长了不少所以最终选择了r8。lora_alpha是缩放系数实际作用是把LoRA的输出乘以alpha/r。在本项目里alpha32、r8缩放比例是4。如果alpha设太高模型在微调初期就会出现明显的“输出漂移”反而让训练不稳定。target_modules指定在哪些模块上插入LoRA。ChatGLM3-6B的注意力层里query_key_value是一个合并后的线性层这是最核心也最常用的注入目标。如果你希望效果更强还可以把dense也加进target_modules但可训练参数量会翻倍需要根据显存情况取舍。3.3 参数量估算为什么只动这两层就够了为了让大家对“LoRA到底省了多少参数”有直观概念我专门算了一下。ChatGLM3-6B的transformer层里hidden_size是4096query_key_value矩阵的维度是12288×4096把Q、K、V三个矩阵拼接在一起这个矩阵约有5030万个参数。当我们在这一层插入LoRAr8时新加的矩阵A维度是8×4096参数3392个实际上8×4096约3.3万矩阵B维度是4096×8同样是约3.3万。也就是说为一个query_key_value模块增加的训练参数不到7万但对于完整的模型来说如果每层都只改query_key_value总可训练参数量大约200万仅占全部参数的0.3%左右。这样的设计带来的好处非常明显训练时反向传播不需要计算绝大部分参数的梯度激活值也只需要保留LoRA相关部分显存占用大幅下降。我之前在同一台24GB显卡上对比过全参微调连加载模型都困难而LoRA微调还能开batch size4训练速度是前者的5倍以上。4. 训练脚本逐段拆解4.1 模型加载与4bit量化配置训练脚本的第一步是加载ChatGLM3-6B原始权重。这里我选择了4bit量化的方式配合bitsandbytes库把模型体积从约12GB压缩到约6GB给后续训练腾出显存空间。import torch from transformers import AutoModel, BitsAndBytesConfig quantization_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_use_double_quantTrue, ) model AutoModel.from_pretrained( THUDM/chatglm3-6b, quantization_configquantization_config, device_mapauto, trust_remote_codeTrue, )这里有一个容易踩的坑ChatGLM3-6B的代码是远程加载的必须设置trust_remote_codeTrue否则程序会在加载过程中直接报错。另外如果显存不是特别紧张建议直接在加载后用model model.bfloat16()把模型转成bfloat16精度这样推理和训练时的数值稳定性更好。4bit量化会带来一点精度损失但对LoRA微调的影响很小因为LoRA是增量学习基座模型的精度只要不是肉眼可见地变笨最终效果都能接受。4.2 初始化LoRA并冻结底座参数模型加载完成后关键一步是使用PEFT库把LoRA层注入模型。PEFT库封装了所有低秩适配的逻辑不用自己手写模块这是我最喜欢它的地方。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha32, target_modules[query_key_value], lora_dropout0.05, biasnone, task_typeTaskType.CAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters()运行print_trainable_parameters之后屏幕上会输出类似“trainable params: 1.8M || all params: 6.2B || trainable%: 0.03”这样的信息。如果你看到可训练参数比例远大于1%说明target_modules配得不合理或者模型没有正确冻结底座参数得回头检查。get_peft_model这个函数会自动把所有原始参数requires_grad设为False只保留LoRA部分的参数更新。所以你在写训练循环的时候不需要手动做任何参数筛选直接用model.parameters()参与优化就行。4.3 训练超参数配置与选择理由训练超参数是整个流程里最值得花时间调试的部分。我的初始配置是from transformers import TrainingArguments training_args TrainingArguments( output_dir./checkpoints, num_train_epochs3, per_device_train_batch_size4, per_device_eval_batch_size4, gradient_accumulation_steps8, warmup_steps100, learning_rate2e-4, weight_decay0.01, logging_steps10, eval_strategysteps, eval_steps200, save_strategysteps, save_steps500, load_best_model_at_endTrue, save_total_limit2, fp16True, report_tonone, )这里有几个参数特别值得解释。learning_rate我取2e-4比全参微调常用的1e-5到3e-5高了一个数量级这是因为LoRA的可训练参数量很少需要更大学习率才能在有限步骤内完成有效更新。如果学习率设太小训练会非常慢且loss下不去设太大则容易震荡。gradient_accumulation_steps8配合batch_size4等效batch size是32。这个值对于5000条数据来说比较合适既保证了梯度更新稳定又不会因为虚拟batch过大导致过拟合。fp16True把训练精度改成半精度能显著降低显存占用并加快训练速度。4.4 完整训练流程与日志解读数据、模型、训练参数都准备好之后就可以直接交给Trainer运行了。我的主训练脚本大约80行核心部分如下from transformers import Trainer trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, tokenizertokenizer, ) trainer.train()在RTX 4090上5000条数据、batch_size4、max_length512的情况下每个epoch大约需要15到20分钟三个epoch总共不到1小时。相比全参微调动辄几小时甚至一天这个速度已经相当理想。我训练过程中重点看三个日志指标train_loss、eval_loss和grad_norm。train_loss在前200步会从6到7左右快速下降到2附近之后逐步趋缓。eval_loss如果在某个step附近开始回升说明模型正在过拟合这时候需要减少epoch数或者增大数据量。grad_norm如果经常超过10说明学习率偏高可以适当调低。如果你想完全掌控训练过程也可以不使用Trainer直接手写训练循环optimizer torch.optim.AdamW(model.parameters(), lr2e-4) for epoch in range(3): for batch in dataloader: outputs model(**batch) loss outputs.loss loss.backward() optimizer.step() optimizer.zero_grad()Trainer的好处是内置了进度条、日志、断点保存适合新手和大多数场景手写循环则更灵活适合需要调试内部状态的场景。我建议第一次跑完流程后用Trainer等熟悉了再考虑手写。5. 推理验证与效果对比5.1 加载微调后的LoRA权重训练完成后输出目录下会生成一个adapter_model.safetensors文件这就是训练得到的LoRA权重大小通常只有几MB到十几MB。这个文件与底座模型权重完全解耦你在推理时只需要先加载原始ChatGLM3-6B再把adapter挂载上去。from transformers import AutoModel, AutoTokenizer from peft import PeftModel base_model_name THUDM/chatglm3-6b adapter_path ./checkpoints tokenizer AutoTokenizer.from_pretrained(base_model_name, trust_remote_codeTrue) model AutoModel.from_pretrained( base_model_name, device_mapauto, trust_remote_codeTrue, ) model PeftModel.from_pretrained(model, adapter_path) model.eval()这里有个重要的细节基础模型加载时不需要再指定quantization_config因为推理阶段我们可以直接用bfloat16精度跑。如果你保留了训练时的4bit配置推理速度会慢不少。PeftModel.from_pretrained会自动加载LoRA权重并完成配置非常方便。5.2 微调前后输出对比为了直观检验微调效果我准备了和训练集分布一致的测试样本分别用基座模型和LoRA微调后的模型做推理。推理调用的方式和ChatGLM3官方一致query 这个手机的电池不太耐用用了两天就没电了官方售后态度也很差。 messages [ {role: user, content: f请判断以下评论的情感倾向只回答正向、负向或中性。\n{query}} ] response, history model.chat(tokenizer, messages[-1][content], history[]) print(response)基座模型在未微调时输出可能是一大段分析文字比如“从评论内容来看用户主要反映了电池续航问题和售后问题这些都是消费者比较关注的方面”它不敢直接给出“负向”这个结论也不稳定。微调后的模型则会直接输出“负向”。这说明什么问题LoRA微调真正改变的是模型的输出风格和任务对齐能力而不是让模型变聪明。基座模型本身已经理解了这句话的含义但不知道你要它做单标签分类。微调就是把它这个能力“引导”到你的任务格式上。我还测了一条训练集里没有出现过的中性评论“正常使用没什么特别好的地方也没明显缺点。”微调后模型正确输出了“中性”。这说明模型学到的是“分类规则”而不是“背答案”泛化能力基本满足项目要求。5.3 评估指标不能只看loss训练loss容易让人产生错觉尤其是当训练集和验证集都很小时。我在验证集上统计了准确率、精确率、召回率和F1分数这比单个loss更有说服力。from sklearn.metrics import classification_report y_true [] y_pred [] for sample in eval_dataset: true_label sample[output] pred_label predict_one(sample) y_true.append(true_label) y_pred.append(pred_label) print(classification_report(y_true, y_pred, target_names[正向, 负向, 中性]))我最后得到的准确率在93%左右负向类别的F1略低于正向和中性这说明负向样本的表述更加多样模型漏判的可能性更大。如果你在业务中遇到类似情况可以考虑补充负向样本或者使用样本加权策略来平衡。6. 常见问题与排查经验6.1 显存不足与训练速度过慢显存不足是新手最容易遇到的问题。如果跑训练时报CUDA out of memory我的排查顺序是先确认是否开了gradient checkpointing再检查batch_size是否过大最后检查序列长度是否超过512。开启gradient checkpointing的方法很简单在Trainer参数里加gradient_checkpointingTrue就行。它以额外的计算开销换取更低的激活内存一次设置立省好几GB显存。如果还不行就把per_device_train_batch_size降到2或1同时把gradient_accumulation_steps相应增大。训练速度过慢时首先检查是不是用到了CPU回退。如果模型部分层被自动分配到CPU上训练速度会断崖式下跌。可以在加载模型时明确设置device_mapcuda:0确保所有层都在GPU上。另外查看任务管理器确认CUDA利用率是否达到70%以上如果很低可能是数据加载环节存在瓶颈可以考虑用DataLoader的num_workers参数增加并行进程。6.2 loss不下降或者一直在震荡loss完全不动是最让人崩溃的问题。我遇到过两种情况一种是因为学习率设得太低LoRA参数更新量太小训练100步后loss只下降了0.01另一种是batch内有过长样本导致padding部分参与计算梯度被大量无意义的token稀释。解决办法对应是把学习率从2e-4逐步试到5e-4观察loss曲线是否开始下降检查tokenize阶段是否把padding的attention_mask正确传入模型。对于后一种情况我更推荐给每条样本单独计算valid token数量在计算loss时用attention_mask把padding部分排除掉。loss震荡的原因一般是学习率过大或者batch size过小。把学习率降回2e-4或者把gradient_accumulation_steps调大震荡通常会缓解。如果你的loss曲线是“下降后回升”那八成是过拟合了需要减小训练轮数或者增加数据量。6.3 LoRA权重不生效的排查还有一个常见问题辛苦训练完的LoRA权重加载后模型输出和基座模型完全一样。这种情况通常是因为推理时没有正确挂载adapter或者加载顺序有问题。我排查时首先检查adapter路径下是否有adapter_config.json文件这个文件记录了LoRA的rank、target_modules等关键参数。如果没有这个文件说明训练时保存不完整需要重新使用trainer.save_model()保存。然后检查PeftModel是否真的注入了LoRA层可以通过以下代码查看for name, param in model.named_parameters(): if param.requires_grad: print(name, param.shape)如果在推理时模型没有任何可训练参数或者LoRA层的名称不存在说明适配器没有正确加载。这时候最简单的办法是重启内核按照“先加载底座模型、再加载adapter”的顺序重新执行一遍。从我实际操作的经验来看LoRA微调ChatGLM3-6B这个组合在数据质量有保障的前提下效果是稳定且可预期的。它最大的价值在于用很小的成本把通用大模型改造成适应你特定任务格式和表达习惯的领域模型。整个流程跑通之后换上自己的数据再做几次超参数调整基本就能落地使用了。本文还有配套的精品资源点击获取
返回列表