基于 transformers + peft 对 Yi-6B-Chat 进行 LoRA 微调:打造甄嬛风格的个性化对话模型(self-llm 实战指南)
2026/9/12 9:54:20 网站建设 项目流程

基于 transformers + peft 对 Yi-6B-Chat 进行 LoRA 微调:打造甄嬛风格的个性化对话模型(self-llm 实战指南)

【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/GitHub_Trending/se/self-llm

本篇文章是《开源大模型食用指南》(self-llm)中 Yi 系列模型实战的一部分,以 04-Yi-6B-Chat Lora 微调.md 为骨架,结合仓库内完整训练脚本与甄嬛对话数据集,系统讲解如何基于 transformers、peft 对 Yi-6B-Chat 进行 LoRA 指令微调,并完成微调后的模型推理。读完本文,你将掌握指令集构建、ChatML 格式数据编码、LoRA 配置、Trainer 训练与 PeftModel 加载推理的完整链路,并能在自己的角色对话数据集上复现该流程。

一、项目背景与前置条件

Yi-6B-Chat 是 01.AI 开源的中英双语对话模型,本教程演示如何基于 transformers、peft 等框架对其进行 LoRA(Low-Rank Adaptation)微调。LoRA 是一种高效微调方法,通过在原始权重矩阵旁注入低秩分解矩阵来大幅减少可训练参数量,其原理可参考公开资料"深入浅出 LoRA"一文(如知乎相关技术博客)做进一步学习,本文侧重实战落地。

需要特别说明的是,本教程的训练脚本未使用分布式框架,属于单卡训练,微调 Yi-6B-Chat 模型至少需要 20G 及以上显存(如 RTX 3090 / 4090 等 24G 显存显卡)。运行脚本前,还需要修改脚本文件中的模型路径数据集路径,使其指向你自己的实际位置。

本文对应的完整可运行脚本位于仓库同级目录 04-Yi-6B-chat Lora微调.py,训练所用甄嬛指令数据集位于 dataset/huanhuan.jsonl(另有 dataset/huanhuan.json 的 JSON 数组格式与 dataset/huanhuan-100.json 的百条小样本版本,便于快速试跑)。

二、环境配置

在完成基础环境配置和本地模型部署的情况下,还需要安装以下第三方库。本教程使用 AutoDL 平台的 3090 等 24G 显存机器,模型路径为/root/autodl-tmp/01ai/Yi-6B-Chat,读者需要将其替换为自己的模型实际存放路径:

pip install transformers==4.35.2 pip install peft==0.4.0 pip install datasets==2.10.1 pip install accelerate==0.20.3 pip install tiktoken pip install transformers_stream_generator

版本说明:教程中的版本号是经过验证的组合(transformers 4.35.2 + peft 0.4.0 + datasets 2.10.1 + accelerate 0.20.3)。若你希望尝试更新版本,可参考仓库中 examples/Chat-嬛嬛/readme.md 中使用的 transformers 4.43.1、peft 0.11.1、datasets 2.20.0、accelerate 0.32.1 组合,但需自行验证 API 兼容性。tiktokentransformers_stream_generator是 Yi 模型依赖的补充库。

三、指令集构建:让模型学会"扮演甄嬛"

LLM 的微调一般指指令微调(Instruction Tuning)过程。所谓指令微调,是指我们使用的微调数据形如:

{ "instruction": "回答以下用户问题,仅输出答案。", "input": "1+1等于几?", "output": "2" }

其中:

  • instruction:用户指令,告知模型其需要完成的任务;
  • input:用户输入,是完成用户指令所必需的输入内容;
  • output:模型应该给出的输出。

核心训练目标是让模型具有理解并遵循用户指令的能力。因此,在指令集构建时,应针对目标任务有针对性地构建任务指令集。例如,本节使用基于《甄嬛传》剧本构建的 Chat-甄嬛 项目作为示例(相关背景与数据处理思路可参考 examples/Chat-嬛嬛/readme.md:从剧本台词中提取目标角色对话、整理成 instruction/input/output 问答对),目标是构建一个能够模拟甄嬛对话风格的个性化 LLM,因此构造的指令形如:

{ "instruction": "现在你要扮演皇帝身边的女人--甄嬛", "input": "你是谁?", "output": "家父是大理寺少卿甄远道。" }

从仓库 dataset/huanhuan.json 中可以看到实际数据形如:

{ "instruction": "娘娘。", "input": "", "output": "你放心,本宫到任何时候都不会自轻自贱委屈了这孩子。" }

input字段可以为空字符串,此时指令与输入会拼接在一起送入模型(详见下文数据格式化部分)。全量数据集共约 3700+ 条甄嬛台词问答对,分布在 dataset/huanhuan.jsonl(每行一条 JSON)中。

四、数据格式化:将文本编码为 ChatML 格式的 input_ids 与 labels

LoRA 训练的数据需要经过格式化、编码之后再输入给模型训练。熟悉 PyTorch 训练流程的同学都知道,一般需要将输入文本编码为input_ids,将输出文本编码为labels,编码后的结果都是多维向量。

我们首先定义一个预处理函数process_func,用于对每一个样本编码其输入、输出文本并返回编码后的字典:

def process_func(example): MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 input_ids, attention_mask, labels = [], [], [] instruction = tokenizer("\n".join(["<|im_start|>system", "现在你要扮演皇帝身边的女人--甄嬛.<|im_end|>" + "\n<|im_start|>user\n" + example["instruction"] + example["input"] + "<|im_end|>\n"]).strip(), add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens response = tokenizer("<|im_start|>assistant\n" + example["output"] + "<|im_end|>\n", 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] # Yi-6B的构造就是这样的 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 }

这段代码的关键点如下:

  1. ChatML 对话模板:Yi-6B-Chat 使用<|im_start|>system / <|im_start|>user / <|im_start|>assistant<|im_end|>作为角色分隔符。系统提示词固定为"现在你要扮演皇帝身边的女人--甄嬛.",用户内容由instruction + input拼接而成,助手内容为output
  2. add_special_tokens=False:不额外在开头添加特殊 token,保证序列完全由我们自己控制。
  3. labels 构造:指令部分(system + user)的标签全部置为-100(交叉熵损失会忽略该值),仅保留 assistant 输出部分参与损失计算,这正是"只让模型学习如何回答、不学习如何提问"的关键。注意labels末尾补的是pad_token_id,这是 Yi-6B 特有的构造方式。
  4. 截断:中文经过分词器后 token 数量偏多,MAX_LENGTH = 384预留了足够长度,超出部分直接截断,保证数据完整性。

随后加载数据集并用上述函数处理数据:

# 将JSON文件转换为CSV文件 import pandas as pd from datasets import Dataset df = pd.read_json('/root/dataset/huanhuan.json') ds = Dataset.from_pandas(df) tokenized_id = ds.map(process_func, remove_columns=ds.column_names)

注意:脚本中读取的是/root/dataset/huanhuan.json,实际运行时要改为仓库内数据集在你自己机器上的绝对路径(如将 dataset/huanhuan.json 拷贝到/root/dataset/下,或直接指向该文件的绝对路径)。

经过格式化后,送入模型的每一条数据都是一个字典,包含input_idsattention_masklabels三个键值对。decode之后应该是这样的完整对话结构:

<|im_start|>system 现在你要扮演皇帝身边的女人--甄嬛.<|im_end|> <|im_start|>user 小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——<|im_end|> <|im_start|>assistant 嘘——都说许愿说破是不灵的。<|im_end|> <|endoftext|>

可以输出一条样本观察验证:

print(tokenizer.decode(tokenized_id[0]['input_ids'])) print(tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[0]["labels"]))))

从输出可以看到,input_ids解码后是完整的"system + user + assistant"对话,而labels过滤掉-100后只保留了 assistant 的回答部分,验证了标签构造的正确性。

五、加载 tokenizer 与半精度模型

import torch from transformers import AutoModelForCausalLM, AutoTokenizer tokenizer = AutoTokenizer.from_pretrained('01ai/Yi-6B-Chat', use_fast=False, trust_remote_code=True) # 模型以半精度形式加载,如果你的显卡比较新的话,可以用torch.bfloat16形式加载 model = AutoModelForCausalLM.from_pretrained('01ai/Yi-6B-Chat', trust_remote_code=True, torch_dtype=torch.half, device_map="auto")
  • use_fast=False:关闭 fast tokenizer,Yi 这类模型更推荐使用原版 slow tokenizer,避免特殊 token 处理差异;
  • torch_dtype=torch.half:以 FP16 半精度加载模型,可显著降低显存占用;若显卡较新(如支持 bf16 的 A100/H100),可改用torch.bfloat16
  • device_map="auto":让 transformers 自动将模型分配到可用设备上(单卡场景即整卡加载)。

训练脚本实际运行时同样通过AutoTokenizer.from_pretrainedAutoModelForCausalLM.from_pretrained加载,模型名01ai/Yi-6B-Chat需替换为你本地模型的路径(如/root/autodl-tmp/01ai/Yi-6B-Chat)。

六、定义 LoraConfig:注入低秩矩阵

LoraConfig中可以设置很多参数,但主要的参数并不多,常用参数及含义如下:

参数含义本教程取值
task_type模型类型TaskType.CAUSAL_LM(因果语言模型)
target_modules需要训练的模型层名字,主要是 attention 部分及 MLP 的投影层;不同模型的层名不同,可传数组、字符串或正则表达式["q_attn", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
rLoRA 的秩(rank),决定低秩矩阵的维度,越小参数越少8
lora_alphaLoRA 的缩放系数 alpha,最终缩放因子为alpha / r32
lora_dropout注入层前的 Dropout 比例,防止过拟合0.1
inference_mode是否处于推理模式False(训练模式)

配置示例:

from peft import LoraConfig, TaskType, get_peft_model config = LoraConfig( task_type=TaskType.CAUSAL_LM, target_modules=["q_attn", "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 比例 )

值得注意target_modules的写法:Yi-6B 沿用了 LLaMA 风格的模型结构,因此这里同时覆盖了自注意力层的q_attn(Yi 中 query 投影的特殊命名)、k_projv_projo_proj,以及 MLP 层的gate_projup_projdown_proj,即对每一层 transformer 块中几乎全部线性投影层注入 LoRA 适配器,使微调容量更大。具体层名可通过打印模型结构或阅读模型配置文件确认。

七、训练模型:get_peft_model + Trainer

首先使用get_peft_model将基础模型和peft_config包装起来,创建PeftModel。要了解模型中可训练参数的数量,可以使用print_trainable_parameters方法:

model = get_peft_model(model, config) model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法 model.print_trainable_parameters()
  • enable_input_require_grads():开启梯度检查点(gradient_checkpointing)时必须执行该方法,否则梯度无法正确回传到输入层;
  • print_trainable_parameters():打印可训练参数量与总参数量占比,LoRA 模式下通常可训练参数只占 1% 左右。

接下来自定义TrainingArguments参数。该类的源码对每个参数都有详细说明,这里介绍几个常用参数:

参数含义本教程取值
output_dir模型的输出路径./output/Yi-6B
per_device_train_batch_size单卡 batch size8
gradient_accumulation_steps梯度累加步数;显存较小时可调小 batch_size、调大梯度累加2
logging_steps每隔多少步输出一次 log10
num_train_epochs训练轮数(epoch)3
gradient_checkpointing梯度检查点,开启后可显著降低显存(以时间换显存),同时必须执行model.enable_input_require_grads()True
save_steps每隔多少步保存一次 checkpoint100
learning_rate学习率1e-4
save_on_each_node多节点训练时每个节点都保存 checkpoint(单卡场景无副作用)True
from transformers import DataCollatorForSeq2Seq, TrainingArguments, Trainer args = TrainingArguments( output_dir="./output/Yi-6B", per_device_train_batch_size=8, gradient_accumulation_steps=2, logging_steps=10, num_train_epochs=3, gradient_checkpointing=True, save_steps=100, learning_rate=1e-4, save_on_each_node=True )

最后,使用Trainer训练模型。注意这里的数据整理器(data collator)使用DataCollatorForSeq2Seq并对序列做 padding,因为每条样本长度不同,需要 padding 到 batch 内最大长度才能拼成张量:

trainer = Trainer( model=model, args=args, train_dataset=tokenized_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) trainer.train()

模型训练完成后,会输出如下所示的信息:

从训练日志可以看到:global_step 到 680 时 train_loss 约为 1.668,690 步时降到 1.646;最终在约 699 步(epoch 约 2.99)时完成训练,整体训练时长约 1212 秒,吞吐约 9.2 samples/s。若数据集规模与这里一致(约 3700 条,batch_size=8、梯度累加 2,等效 batch 16,共 3 个 epoch),训练步数约在 700 步左右,因此save_steps=100会得到checkpoint-100checkpoint-700共 7 个 checkpoint。

八、模型推理:加载 PeftModel 并生成对话

训练好的模型被保存在./output/Yi-6B目录下。如果想要从头加载微调好的模型,需要执行下面的代码:

from transformers import AutoModelForSeq2SeqLM from peft import PeftModel, PeftConfig peft_model_id = "output/Yi-6B/checkpoint-600" # 这里我训练出效果最好的一版是 checkpoint-600,所以调用了这个,大家可以根据自己情况选择 config = PeftConfig.from_pretrained(peft_model_id) model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path) model = PeftModel.from_pretrained(model, peft_model_id)

注意:原文档中加载基座模型使用了AutoModelForSeq2SeqLM,对于 Yi-6B-Chat 这种因果语言模型,正确的加载方式应使用AutoModelForCausalLMPeftConfig会自动从 checkpoint 中读取base_model_name_or_path(即原始基座模型路径),因此加载基座模型前无需再手动指定模型名。这是实际运行中容易踩坑的地方。

然后使用以下代码进行模型推理:

model.eval() input = tokenizer("<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n".format("你是谁?", "").strip() + "\nassistant\n ", return_tensors="pt").to(model.device) max_length = 512 outputs = model.generate( **input, max_length=max_length, eos_token_id=7, do_sample=True, repetition_penalty=1.3, no_repeat_ngram_size=5, temperature=0.1, top_k=40, top_p=0.8, ) print(tokenizer.decode(outputs[0], skip_special_tokens=True))

推理时的关键参数说明:

  • eos_token_id=7:Yi-6B-Chat 的结束符<|endoftext|>对应的 token id 为 7,指定后模型生成到该 token 即停止;
  • repetition_penalty=1.3:重复惩罚系数,>1 时抑制重复生成;
  • no_repeat_ngram_size=5:禁止 5-gram 级别的重复片段;
  • temperature=0.1:低温度使采样更集中、更保守,符合角色人设对话的稳定性需求;
  • top_k=40top_p=0.8:结合 top-k 与 top-p 采样的截断策略;
  • skip_special_tokens=True:解码时过滤<|im_start|><|im_end|>等特殊 token。

从推理输出可以看到,微调后的模型在给定系统提示"现在你要扮演皇帝身边的女人--甄嬛."后,对"你是谁?"的回答为"我是甄嬛,家父是大理寺少卿甄远道。",与数据集中人物设定一致,说明模型已经学到了甄嬛的角色身份与语言风格。

九、完整脚本速览与运行步骤

仓库中的 04-Yi-6B-chat Lora微调.py 将上述所有步骤整合为单个可执行脚本,其结构与文档完全对应:

  1. 定义process_func数据预处理函数;
  2. 定义LoraConfigTrainingArguments
  3. __main__中依次完成:读取 JSON 数据集 → 加载 tokenizer → map 编码数据集 → 半精度加载基座模型 →enable_input_require_gradsget_peft_modelTrainer.train()→ 推理验证。

运行前需要修改两处路径:

  • 数据集路径:脚本中为/root/dataset/huanhuan.json,请改为仓库 dataset/huanhuan.json 在你机器上的绝对路径(快速验证可用仅 100 条的 dataset/huanhuan-100.json);
  • 模型路径:脚本中为'01ai/Yi-6B-Chat',请改为本地模型路径(如/root/autodl-tmp/01ai/Yi-6B-Chat)。

十、常见问题与注意事项

  1. 显存不足:本教程未使用分布式框架,至少需要 20G 以上显存。显存紧张时可降低per_device_train_batch_size(如 4 或 2)并相应增大gradient_accumulation_steps(如 4 或 8),保持等效 batch size 不变;或改用torch.bfloat16(需显卡支持)进一步降低显存。
  2. 基座模型加载方式:推理阶段加载微调模型时,基座模型应使用AutoModelForCausalLM而非AutoModelForSeq2SeqLM(原文档该处存在笔误),PeftConfig.from_pretrained会从 checkpoint 中自动读出base_model_name_or_path
  3. eos_token_id=7:该取值对应 Yi-6B-Chat 的<|endoftext|>token,若替换为其他模型需重新确认其 eos token id。
  4. enable_input_require_grads必须调用:开启gradient_checkpointing=True后若不执行该方法,训练会因输入层无梯度需求而报错。
  5. 数据格式一致性:微调与推理阶段必须使用相同的 ChatML 模板与系统提示词,否则模型输出的角色风格会不稳定。

至此,你已经完整走通了"数据准备 → 数据格式化 → LoRA 配置 → 训练 → 推理"的 Yi-6B-Chat 个性化微调全流程。将该流程中的甄嬛数据集替换为任意角色对话数据(构建方法可参考 examples/Chat-嬛嬛/readme.md),即可快速打造属于自己的角色对话模型。

【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/GitHub_Trending/se/self-llm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询