self-llm 实战:基于 Hugging Face transformers 与 PEFT 对 GLM-4-9B-Chat-hf 进行 LoRA 微调
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
本指南以《开源大模型食用指南》(self-llm)项目中的 GLM-4 模型教程为主线,完整演示如何在 24G 显存显卡上,基于transformers >= 4.46.0、peft与datasets等框架,对 Hugging Face 标准化的 GLM-4-9B-Chat-hf 模型执行 LoRA 指令微调,并以「甄嬛」风格对话数据集为例,训练出具有特定人设的个性化大模型。读完本文,你将掌握 GLM-4-9B 对话模型的下载、指令数据格式化、LoRA 参数配置、Trainer 训练、权重保存与加载推理的全链路实操能力。
本文基于 models/GLM-4/05-GLM-4-9B-chat-hf Lora 微调.md 撰写,配套可运行的 Notebook 位于 05-GLM-4-9B-chat-hf Lora 微调.ipynb,训练数据为仓库根目录下的 huanhuan.json。
前置说明:为什么要用 GLM-4-9B-chat-hf
GLM-4-9B 系列对话模型(不含视觉模型)的实现已经被 Hugging Facetransformers官方合并并标准化。自transformers 4.46.0 版本起,原有的GLM4-9B-chat模型(依赖trust_remote_code加载自定义代码)可能无法正常使用。因此本教程使用官方标准化的ZhipuAI/glm-4-9b-chat-hf权重,并强制要求transformers>=4.46.0,这正是本文与仓库中另一篇 05-GLM-4-9B-chat Lora 微调.md 的核心差异:
| 对比项 | 05-GLM-4-9B-chat(旧版) | 05-GLM-4-9B-chat-hf(本教程) |
|---|---|---|
| 模型权重 | ZhipuAI/glm-4-9b-chat | ZhipuAI/glm-4-9b-chat-hf |
| transformers 版本 | >=4.40.0 | >=4.46.0 |
| LoRA 目标模块 | query_key_value、dense、dense_h_to_4h、dense_4h_to_h | q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj |
从模型结构上看(可参见 Notebook 中model的打印输出),标准化后的模型由ChatGLMForConditionalGeneration包装:transformer内含word_embeddings(词表 151552)、rotary_pos_emb(旋转位置编码)与 40 层GLMBlock,每个块包含self_attention(query_key_value将 4096 维映射到 4608 维、dense输出投影)与MLP(dense_h_to_4h映射到 27392 维、dense_4h_to_h还原)。这决定了 LoRA 适配器应挂载在哪一层上。
环境准备:在 AutoDL 上租用 24G 显存机器
在 AutoDL 平台租赁一台3090 等 24G 显存的显卡机器,创建实例时按如下配置选择镜像:
- 框架名称:
PyTorch - 框架版本:
2.1.0 - Python 版本:
3.10 (ubuntu22.04) - CUDA 版本:
12.1
实例创建完成后,打开 JupyterLab 并启动其中的终端,后续的环境配置、模型下载与训练演示均在终端内完成。
环境配置:安装依赖库
在完成基本环境配置后,还需安装以下第三方库(本教程所有关键版本均被钉死,避免版本漂移导致兼容性问题):
python -m pip install --upgrade pip # 更换 pypi 源加速库的安装 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install modelscope==1.9.5 pip install "transformers>=4.46.0" pip install streamlit==1.24.0 pip install sentencepiece==0.1.99 pip install accelerate==0.29.3 pip install datasets==2.19.0 pip install peft==0.10.0 pip install tiktoken==0.7.0 MAX_JOBS=8 pip install flash-attn --no-build-isolation各依赖的作用:
modelscope:国内模型下载通道,用于拉取glm-4-9b-chat-hf权重;transformers>=4.46.0:必须满足 4.46.0 及以上,否则 GLM-4-9B 官方标准化实现无法正确加载;peft:提供LoraConfig、PeftModel等 LoRA 微调核心 API;datasets:将 JSON 指令集转换为Dataset对象并做批量预处理;accelerate:支撑device_map="auto"的多设备自动调度与 Trainer 底层加速;sentencepiece/tiktoken:tokenizer 分词所需的底层依赖;flash-attn:Flash Attention 加速库,安装耗时较长(约十几分钟),属正常现象。
注意:若在 Notebook 中执行,还需确认
flash-attn已成功编译安装,训练时若显存紧张可先不启用 flash attention。
数据集准备与模型下载
本教程使用仓库根目录下的 huanhuan.json(共 3729 条对话样本)作为微调数据集,目标是让模型学会「甄嬛」风格的应答。
使用modelscope的snapshot_download函数下载模型,第一个参数为模型名称,cache_dir为模型下载路径。在/root/autodl-tmp下新建model_download.py:
import torch from modelscope import snapshot_download, AutoModel, AutoTokenizer import os model_dir = snapshot_download('ZhipuAI/glm-4-9b-chat-hf', cache_dir='/root/autodl-tmp/glm-4-9b-chat', revision='master')保存后执行python /root/autodl-tmp/model_download.py即可完成下载,模型最终会存放在/root/autodl-tmp/glm-4-9b-chat/ZhipuAI/glm-4-9b-chat目录下(后续加载路径与此对应)。
指令集构建:让模型学会遵循指令
LLM 的微调一般指指令微调(Instruction Tuning),即训练数据形如:
{ "instruction": "回答以下用户问题,仅输出答案。", "input": "1+1等于几?", "output": "2" }字段语义:
instruction:用户指令,告知模型需要完成的任务;input:用户输入,是完成任务所必需的输入内容;output:模型应当给出的标准输出。
核心训练目标是让模型获得理解并遵循用户指令的能力,因此指令集必须围绕目标任务针对性构建。本教程以「模拟甄嬛对话风格的个性化 LLM」为目标,构造的指令形如:
{ "instruction": "你是谁?", "input": "", "output": "家父是大理寺少卿甄远道。" }打开 huanhuan.json 可以看到全部指令数据,例如:
{ "instruction": "娘娘。", "input": "", "output": "你放心,本宫到任何时候都不会自轻自贱委屈了这孩子。" }数据格式化:构造 input_ids / attention_mask / labels
LoRA 训练数据需要经过格式化、编码后才送入模型。熟悉 PyTorch 训练流程的同学都知道:输入文本需编码为input_ids,输出文本需编码为labels(用于计算交叉熵损失),编码结果均为多维向量。为此定义预处理函数process_func,对每个样本编码输入、输出文本并返回编码字典:
def process_func(example): MAX_LENGTH = 384 input_ids, attention_mask, labels = [], [], [] instruction = tokenizer((f"[gMASK]<sop><|system|>\n假设你是皇帝身边的女人--甄嬛。<|user|>\n" f"{example['instruction']+example['input']}<|assistant|>\n" ), add_special_tokens=False) response = tokenizer(f"{example['output']}", add_special_tokens=False) input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为eos token咱们也是要关注的所以 补充为1 labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] if len(input_ids) > MAX_LENGTH: # 做一个截断 input_ids = input_ids[:MAX_LENGTH] attention_mask = attention_mask[:MAX_LENGTH] labels = labels[:MAX_LENGTH] return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels }这段代码的关键设计点:
- Prompt 模板:GLM-4-9B-Chat 采用的模板格式为
[gMASK]<sop>开头,后接<|system|>(系统提示)、<|user|>(用户输入)、<|assistant|>(模型回答)三段式结构:
[gMASK]<sop><|system|> 假设你是皇帝身边的女人--甄嬛。<|user|> 小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——<|assistant|> 嘘——都说许愿说破是不灵的。<|endoftext|>- labels 掩码:
instruction部分(系统提示 + 用户输入)的 labels 置为-100,在 PyTorch 交叉熵损失中-100会被自动忽略,只有response(模型输出)部分参与损失计算,这是指令微调"只学回答、不学提问"的标准做法; - 结尾补 pad_token_id:在
input_ids、attention_mask与labels末尾各追加一个 token,保证序列以 pad 符闭合; - 截断策略:
MAX_LENGTH=384,超过则直接裁剪,防止超长样本撑爆显存。
在 Notebook 中,通过ds.map(process_func, remove_columns=ds.column_names)对整个数据集批量处理,最终得到一个包含input_ids、attention_mask、labels三列、共3729 行的Dataset;用tokenizer.decode可以验证模板拼接正确性(如[151331, 151333, 151335]正是[gMASK] <sop> <|system|>的 token 序列)。
加载 tokenizer 与半精度模型
模型以**半精度(bfloat16)**加载;若显卡较新,可用torch.bfloat16获得更好的数值稳定性。对于通过trust_remote_code加载的自定义实现,必须将trust_remote_code置为True:
tokenizer = AutoTokenizer.from_pretrained('/root/autodl-tmp/glm-4-9b-chat/ZhipuAI/glm-4-9b-chat', use_fast=False, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained('/root/autodl-tmp/glm-4-9b-chat/ZhipuAI/glm-4-9b-chat', device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True)参数说明:
device_map="auto":由 accelerate 自动将 10 个 checkpoint shard 分配到可用设备(Notebook 中可见 "Loading checkpoint shards: 0/10"),支持单卡或多卡;torch_dtype=torch.bfloat16:以 BF16 半精度加载,单卡 24G 显存即可容纳 9.4B 参数;use_fast=False:使用经典(非 fast)tokenizer,与 GLM-4 分词器兼容性更好;trust_remote_code=True:允许执行仓库内自定义代码(模型与分词器实现)。
加载后,若启用梯度检查点(gradient_checkpointing=True),还需执行model.enable_input_require_grads(),为冻结权重下的输入张量保留梯度通路(Notebook 中有对应调用)。
定义 LoraConfig:挂载哪些层、秩与缩放
LoraConfig可配置参数很多,核心参数如下:
task_type:模型类型,因果语言模型填TaskType.CAUSAL_LM;target_modules:需要注入 LoRA 适配器的层名,主要是 attention 与 MLP 中的线性层,可传数组、字符串或正则表达式;r:LoRA 的秩(rank),控制低秩矩阵的维度;lora_alpha:LoRA 的缩放因子 alpha;lora_dropout:适配器中的 Dropout 比例,用于缓解过拟合;inference_mode=False:声明当前为训练模式。
需要特别澄清 LoRA 的缩放机制:实际缩放系数是lora_alpha / r,而不是r本身。本配置中lora_alpha=32、r=8,缩放系数为4 倍。
config = LoraConfig( task_type=TaskType.CAUSAL_LM, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], inference_mode=False, # 训练模式 r=8, # Lora 秩 lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理 lora_dropout=0.1# Dropout 比例 )注意:由于本教程面向 transformers 4.46.0 标准化的glm-4-9b-chat-hf,target_modules使用的是与 LLaMA 系一致的q_proj / k_proj / v_proj / o_proj / gate_proj / up_proj / down_proj命名(可对照 examples/Chat-嬛嬛/train.py 中 Llama-3.1 微调的写法,两者模块名完全相同);而旧版非 hf 模型使用query_key_value、dense、dense_h_to_4h、dense_4h_to_h,两者不可混用。
将配置应用到模型后,用get_peft_model包装并打印可训练参数占比。从 Notebook 的实际运行结果看:
trainable params: 21,176,320 || all params: 9,421,127,680 || trainable%: 0.2248即在约 94.2 亿总参数中,仅约2118 万(0.22%)的参数参与训练,这正是 LoRA 高效微调的核心体现——冻结全部基座权重,只训练注入的低秩适配器,显著降低显存与算力开销。
自定义 TrainingArguments:训练超参
TrainingArguments的源码对每个参数都有详细注释,这里说明本教程用到的几个关键项:
output_dir:模型输出(checkpoint、日志)路径;per_device_train_batch_size:单卡 batch size;gradient_accumulation_steps:梯度累加步数,显存较小时可调小 batch size、调大累加步数,等效放大 batch;logging_steps:每多少步输出一次 log;num_train_epochs:训练轮数;save_steps:每多少步保存一次 checkpoint;learning_rate:学习率;save_on_each_node:多节点训练时每节点各自保存;gradient_checkpointing:梯度检查点,开启后必须先执行model.enable_input_require_grads()(原理是牺牲少量计算时间换取激活值不驻留显存,可大幅降低显存占用)。
args = TrainingArguments( output_dir="./output/GLM4", per_device_train_batch_size=1, gradient_accumulation_steps=8, logging_steps=50, num_train_epochs=2, save_steps=100, learning_rate=1e-5, save_on_each_node=True, gradient_checkpointing=True )本配置下实际等效 batch size 为1 × 8 = 8。从 Notebook 的训练输出可见:共执行932 步、2 个 epoch,训练 loss 由初期的 3.84 稳步下降至 2.61 附近,最终train_loss=2.836,训练耗时约 21 分钟,说明该超参组合在 24G 显存下收敛平稳、开销可控。
使用 Trainer 训练
组装Trainer并开始训练:
trainer = Trainer( model=model, args=args, train_dataset=tokenized_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) trainer.train()要点说明:
train_dataset传入经process_func预处理后的tokenized_id;data_collator使用DataCollatorForSeq2Seq,训练时会按 batch 内最长序列做padding,并自动将labels中的-100与pad_token_id对齐(避免 padding 位置参与损失计算);trainer.train()内部由 transformers 封装了前向、反向、优化器更新与日志记录全流程,无需手写训练循环。
保存 LoRA 权重
训练完成后,只需保存 LoRA 适配器与 tokenizer,无需保存 9.4B 的完整基座权重(PeftModel 保存的仅是注入的低秩矩阵,体积通常在几十 MB 量级):
lora_path='./GLM4' trainer.model.save_pretrained(lora_path) tokenizer.save_pretrained(lora_path)从 Notebook 输出可见,tokenizer.save_pretrained会在./GLM4_lora目录下生成tokenizer_config.json、special_tokens_map.json、tokenizer.model、added_tokens.json等文件,与适配器权重共同构成一份可独立分发的 LoRA 产物。
加载 LoRA 权重推理
训练完成后,用PeftModel将 LoRA 适配器挂载回基座模型进行推理:
from transformers import AutoModelForCausalLM, AutoTokenizer import torch from peft import PeftModel mode_path = '/root/autodl-tmp/glm-4-9b-chat/ZhipuAI/glm-4-9b-chat' lora_path = './GLM4_lora' # 加载tokenizer tokenizer = AutoTokenizer.from_pretrained(mode_path, trust_remote_code=True) # 加载模型 model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True).eval() # 加载lora权重 model = PeftModel.from_pretrained(model, model_id=lora_path) prompt = "你是谁?" inputs = tokenizer.apply_chat_template([{"role": "system", "content": "假设你是皇帝身边的女人--甄嬛。"},{"role": "user", "content": prompt}], add_generation_prompt=True, tokenize=True, return_tensors="pt", return_dict=True ).to('cuda') gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1} with torch.no_grad(): outputs = model.generate(**inputs, **gen_kwargs) outputs = outputs[:, inputs['input_ids'].shape[1]:] print(tokenizer.decode(outputs[0], skip_special_tokens=True))关键细节:
- 对话模板构造:使用
tokenizer.apply_chat_template按system / user角色消息自动套用 GLM-4 的 chat 模板,add_generation_prompt=True会在末尾追加<|assistant|>以触发生成,return_dict=True使inputs直接可作为model.generate的 kwargs; - 生成参数:
max_length=2500限制最大生成长度,do_sample=True开启采样,top_k=1等价于贪心解码(只取概率最高的 token); - 输出截取:
outputs[:, inputs['input_ids'].shape[1]:]去掉输入部分,只保留新生成的 token; - 效果验证:Notebook 的实际运行结果为
我是甄嬛,家父是大理寺少卿甄远道。,说明模型已成功习得「甄嬛」人设,LoRA 微调链路完整打通。
总结与延伸
至此,一条完整的 GLM-4-9B-Chat-hf LoRA 微调链路已经跑通:环境准备 → 依赖安装 → 模型下载 → 指令集构建 → 数据格式化 → 半精度加载 → LoraConfig → TrainingArguments → Trainer 训练 → 权重保存 → PeftModel 推理。全文涉及的核心文件均可从仓库直接获取:
- 教程文档与 Notebook:models/GLM-4/05-GLM-4-9B-chat-hf Lora 微调.md、05-GLM-4-9B-chat-hf Lora 微调.ipynb;
- 训练数据集:dataset/huanhuan.json;
- 同类微调参考实现:examples/Chat-嬛嬛/train.py(Llama-3.1-8B-Instruct 版,可对照理解 LoRA 流程的通用性);
- 旧版(非 hf)GLM-4 LoRA 教程:05-GLM-4-9B-chat Lora 微调.md。
需要留意的是:本教程基于transformers>=4.46.0与glm-4-9b-chat-hf权重展开,若更换模型版本或 transformers 版本,target_modules的层名与 Prompt 模板格式均需同步核对;24G 显存是单卡运行本方案的前提,显存更小时可进一步调低per_device_train_batch_size并调高gradient_accumulation_steps。掌握了这条 LoRA 流水线,你可以将其迁移到任意角色人设、领域指令或对话风格数据上,快速定制专属大模型。
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考