基于《甄嬛传》剧本的个性化 AI 实战:Chat-嬛嬛 LoRA 微调全流程(self-llm 项目案例)
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
本篇文章完整复现 Datawhale/self-llm 仓库中的 Chat-嬛嬛 案例:从《甄嬛传》剧本原始台词出发,经过角色对话抽取、指令数据集构造、LLaMA3.1-8B-Instruct 模型 LoRA 微调、再到加载 LoRA 权重与"嬛嬛"对话,打通"小说/剧本 → 角色人设 AI"的个性化大模型训练完整链路。读完本文,你将掌握角色对话数据构造方法、LoRA 训练脚本的每一处关键参数,以及如何把这套流程迁移到任意角色、任意基座模型上。
项目背景:Chat-嬛嬛 与个性化 AI 的思路
Chat-嬛嬛(又名 Chat-甄嬛)是利用《甄嬛传》剧本中所有关于甄嬛的台词和语句,基于大模型进行LoRA 微调得到的、能够模仿甄嬛语气的聊天语言模型。甄嬛是小说《后宫·甄嬛传》与电视剧《甄嬛传》的女一号,其人物弧光从入宫选秀、斗垮华妃,到遭皇后暗算、出宫为尼,再到复宠回宫、扳倒幕后黑手,最终成为权倾朝野的圣母皇太后——丰富且风格鲜明的台词,使其成为极具辨识度的角色微调语料。
该案例的深层价值在于:它并非只为复刻一个"嬛嬛",而是以《甄嬛传》为切入点,打造了一套基于小说、剧本的个性化 AI 微调大模型完整流程——提供任一小说或剧本,指定人物角色,运行本项目完整流程,即可让每位用户基于心仪的作品打造一个契合角色人设、具备高度智能的个性化 AI。据原项目文档记载,Chat-嬛嬛 模型累计下载量 15.6k+,并先后获得 2023 讯飞星火杯人认知大模型场景创新赛 Top50、2024 书生·浦语大模型挑战赛(春季赛)创意应用奖 Top12。
整体技术路线概览
整个案例分为四个步骤,与本仓库 Examples 导读 中"实际应用示范"的定位一致:
- 环境准备:搭建 Python/PyTorch/依赖库环境;
- 数据准备:从剧本台词中抽取角色对话,构造
instruction/input/output指令数据集; - 模型训练:基于 LLaMA3.1-8B-Instruct 调用 LoRA 微调脚本训练;
- 推理测试:加载 LoRA 权重,与角色对话验证效果。
对应到仓库文件,本案例的核心资产集中在 examples/Chat-嬛嬛/(教程文档与 train.py 训练脚本)和 dataset/(甄嬛指令数据集),下文将逐一深入。
Step 1:环境准备
本文的基础环境如下:
---------------- ubuntu 22.04 python 3.12 cuda 12.1 pytorch 2.3.0 ----------------默认学习者已安装好以上 Pytorch(CUDA)环境,如未安装请自行安装。
首先pip换源加速下载并安装依赖包:
# 升级pip python -m pip install --upgrade pip # 更换 pypi 源加速库的安装 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install modelscope==1.16.1 pip install transformers==4.43.1 pip install accelerate==0.32.1 pip install peft==0.11.1 pip install datasets==2.20.0各依赖在本流程中的职责如下:
- modelscope:从 ModelScope 模型库下载 LLaMA3.1 基座模型;
- transformers:加载模型与 tokenizer、定义
TrainingArguments、驱动Trainer训练; - peft:提供
LoraConfig、get_peft_model、PeftModel,是 LoRA 微调与推理加载的核心库; - datasets:将处理好的数据包装为
Dataset供 Trainer 消费; - accelerate:Trainer 的底层分布式/显存管理支撑。
与仓库中 LLaMA3.1 Lora 微调教程 相比,本案例依赖版本一致(transformers 4.43.x、accelerate 0.32.1、peft 0.11.1、datasets 2.20.0),可直接复用同一套环境。
Step 2:数据准备:从剧本台词到指令数据集
2.1 分析原始剧本格式
《甄嬛传》剧本数据的原始形态是"场景 + 人物名 + 台词"的纯文本,例如:
第2幕 (退朝,百官散去) 官员甲:咱们皇上可真是器重年将军和隆科多大人。 官员乙:隆科多大人,恭喜恭喜啊!您可是国家的大功臣啊! 官员丙:年大将军,皇上对你可是垂青有加呀! 官员丁:年大人,您可是皇上的股肱之臣哪! 苏培盛(追上年羹尧):年大将军请留步。大将军—— 年羹尧:苏公公,有何指教? 苏培盛:不敢。皇上惦记大将军您的臂伤,特让奴才将这秘制的金创药膏交给大人,叫您使用。 年羹尧(遥向金銮殿拱手):臣年羹尧恭谢皇上圣恩!敢问苏公公,小妹今日在宫中可好啊? 苏培盛:华妃娘娘凤仪万千、宠冠六宫啊,大将军您放心好了。 年羹尧:那就有劳苏公公了。(转身离去) 苏培盛:应该的。这种"人物名:台词"的结构非常规整,每一句都有人物及对应台词,因此可以很轻松地将其处理成对话形式的数据:
[ {"role":"官员甲", "content":"咱们皇上可真是器重年将军和隆科多大人。"}, {"role":"官员乙", "content":"隆科多大人,恭喜恭喜啊!您可是国家的大功臣啊!"}, {"role":"官员丙", "content":"年大将军,皇上对你可是垂青有加呀!"}, {"role":"官员丁", "content":"年大人,您可是皇上的股肱之臣哪!"}, {"role":"苏培盛", "content":"年大将军请留步。大将军——"}, ... ]2.2 抽取目标角色,构造 QA 问答对
在上述对话列表的基础上,进一步筛选出我们所关注的角色(甄嬛)的对话,并将其组织为instruction/input/output的问答对:前文他人的台词作为instruction,甄嬛的回应作为output,input一般为空。对于此类规整数据,使用正则表达式等方法即可快速提取并抽取出目标角色的对话。
[ { "instruction": "小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——", "input": "", "output": "嘘——都说许愿说破是不灵的。" }, { "instruction": "这个温太医啊,也是古怪,谁不知太医不得皇命不能为皇族以外的人请脉诊病,他倒好,十天半月便往咱们府里跑。", "input": "", "output": "你们俩话太多了,我该和温太医要一剂药,好好治治你们。" }, { "instruction": "嬛妹妹,刚刚我去府上请脉,听甄伯母说你来这里进香了。", "input": "", "output": "出来走走,也是散心。" } ]2.3 无规整格式时的处理思路:借助大模型抽取对话
很多情况下我们并没有剧本这样优秀的台词格式数据,而只有一段段的叙述性文本。例如《西游记白话文》的文本形态是:
...... 原来孙悟空走了以后,有一个混世魔王独占了水帘洞,并且抢走了许多猴子猴孙。孙悟空听到这些以后,气得咬牙跺脚。他问清了混世魔王的住处,决定找混世魔王报仇,便驾着筋斗云,朝北方飞去。 不一会儿,孙悟空就来到混世魔王的水脏洞前,对门前的小妖喊到∶"你家那个狗屁魔王,多次欺负我们猴子。我今天来,要和那魔王比比高低! "小妖跑进洞里,报告魔王。魔王急忙穿上铁甲,提着大刀,在小妖们的簇拥下走出洞门。 孙悟空赤手空拳,夺过了混世魔王的大刀,把他劈成了两半。然后,拔下一把毫毛咬碎喷了出去,毫毛变成许多小猴子,直杀进洞里,把所有的妖精全杀死,然后救出被抢走的小猴子,放了一把火烧了水脏洞。 ......这类文本中角色对话与叙述混杂,就需要借助大模型的能力,从整段文本中提取出"角色 + 角色对应的对话",然后再筛选出我们需要的角色对话。可以借助extract-dialogue这类开源对话抽取小工具,将叙述文本批量转化为结构化对话数据。
2.4 仓库数据集实测:三种文件形态
本仓库在 dataset/ 下提供了最终整理好的甄嬛指令数据集,共三种形态可供对照:
- dataset/huanhuan.json:约 1.8 万行,标准 JSON 数组格式,每个元素为
{instruction, input, output},是训练主数据; - dataset/huanhuan-100.json:约 500 行的精简版本,适合先跑通流程、快速验证;
- dataset/huanhuan.jsonl:约 3700 行的 JSONL(JSON Lines)格式,每行一个完整 JSON 对象,便于流式读取。
三者的字段结构完全一致,例如huanhuan.jsonl中的原始记录:
{"instruction":"娘娘。","input":"","output":"你放心,本宫到任何时候都不会自轻自贱委屈了这孩子。"} {"instruction":"你是谁?","input":"","output":"我是甄嬛,家父是大理寺少卿甄远道。"} {"instruction":"嬛嬛,若你肯,你还是朕的宠妃,朕待你还和从前一样。","input":"","output":"从前?皇上以为还能回到从前吗?"}可以看到,训练语料完整覆盖了甄嬛在不同人生阶段的回应风格——既有对下人的安抚,也有对皇上的绵里藏针,这正是角色人设 AI 的数据基础。
2.5 数据增强:让角色对话更丰富
除了从剧本直接抽取,数据准备环节还可以增加数据增强:例如利用两到三条已有数据作为 example 丢给 LLM,让其生成风格类似的新数据;或者找一部分日常对话的数据集,使用 RAG 生成一些固定角色风格的对话数据。这一步可以完全放开大胆尝试,是提升角色泛化能力的重要手段。
Step 3:模型训练:基于 LLaMA3.1-8B-Instruct 的 LoRA 微调
数据准备完成后,训练环节与 self-llm 中其他模型的 LoRA 微调模块高度一致——将数据处理成所需格式,调用训练脚本即可。本案例选择LLaMA3.1-8B-Instruct作为基座模型。
3.1 模型下载
首先下载模型,创建model_download.py文件,输入以下内容:
import torch from modelscope import snapshot_download, AutoModel, AutoTokenizer import os model_dir = snapshot_download('LLM-Research/Meta-Llama-3.1-8B-Instruct', cache_dir='/root/autodl-tmp', revision='master')注意:记得修改
cache_dir为你的模型下载路径。
这里使用 ModelScope 的snapshot_download函数,第一个参数为模型名称,cache_dir为模型下载路径,revision='master'指定主分支版本。下载完成后模型缓存目录名会将模型 ID 中的.转为___(如LLM-Research/Meta-Llama-3___1-8B-Instruct),后续训练脚本与推理脚本中的路径需与之对应。
3.2 训练脚本 train.py 源码解析
本案例在 examples/Chat-嬛嬛/train.py 中提供了可直接运行的训练脚本,只需修改其中的数据集路径和模型路径即可。下面逐段剖析其实现。
① 数据加载:JSON → pandas → HuggingFace Dataset
# 将JSON文件转换为CSV文件(此处实际为读入 DataFrame) df = pd.read_json('huanhuan.json') ds = Dataset.from_pandas(df) tokenized_id = ds.map(process_func, remove_columns=ds.column_names)脚本用pd.read_json读取 dataset/huanhuan.json(或你自定义的角色数据集),转换为 pandas DataFrame,再通过Dataset.from_pandas包装为 HuggingFaceDataset,最后用ds.map(process_func, remove_columns=ds.column_names)对每条样本执行格式化处理并移除原始列。
② process_func:指令数据格式化与 tokenize
process_func是数据管线的核心,负责把每一条{instruction, input, output}编码成模型训练所需的input_ids / attention_mask / labels:
def process_func(example): MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 input_ids, attention_mask, labels = [], [], [] instruction = tokenizer(f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\nCutting Knowledge Date: December 2023\nToday Date: 26 Jul 2024\n\n现在你要扮演皇帝身边的女人--甄嬛<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{example['instruction'] + example['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens response = tokenizer(f"{example['output']}<|eot_id|>", 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 }关键设计点:
- LLaMA3.1 Chat Template:
instruction部分按 LLaMA3.1 的对话模板拼接,依次包含<|begin_of_text|>起始符、<|start_header_id|>system<|end_header_id|>系统角色(其中嵌入 LLaMA3 系列默认的Cutting Knowledge Date: December 2023 / Today Date: ...与角色设定"现在你要扮演皇帝身边的女人--甄嬛")、user用户输入,最后以assistant头结尾等待模型续写。对应模板如下:
<|begin_of_text|><|start_header_id|>system<|end_header_id|> 现在你要扮演皇帝身边的女人--甄嬛<|eot_id|><|start_header_id|>user<|end_header_id|> 你好呀<|eot_id|><|start_header_id|>assistant<|end_header_id|> 你好,我是甄嬛,你有什么事情要问我吗?<|eot_id|><|start_header_id|>assistant<|end_header_id|>- add_special_tokens=False:tokenize 时不再自动添加 BOS 等特殊 token,因为模板中已手动写入了
<|begin_of_text|>; - labels 掩码:
instruction部分的 labels 全部置为-100(PyTorch 交叉熵损失会忽略该值),只有output部分的 token 参与损失计算——这是"只学习回答、不学习提问"的标准做法; - MAX_LENGTH=384:Llama 分词器会将一个中文字切分为多个 token,因此需要放开一些最大长度以保证数据完整性,超长样本统一截断到 384;
- 末尾补充
pad_token_id,并将对应位置 attention_mask 置为 1,保证序列对齐。
③ 加载模型与 tokenizer
model = AutoModelForCausalLM.from_pretrained('./LLM-Research/Meta-Llama-3___1-8B-Instruct', device_map="auto",torch_dtype=torch.bfloat16) model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法 tokenizer = AutoTokenizer.from_pretrained('./LLM-Research/Meta-Llama-3___1-8B-Instruct', use_fast=False, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token模型以bfloat16 半精度加载,device_map="auto"自动分配设备;开启gradient_checkpointing时必须调用model.enable_input_require_grads(),否则前向传播中的非叶子张量无法获得梯度;同时将pad_token指向eos_token,避免 padding 时报错。
④ LoraConfig:LoRA 核心参数
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 比例 ) model = get_peft_model(model, config) model.print_trainable_parameters() # 打印总训练参数各参数含义与影响:
| 参数 | 取值 | 说明 |
|---|---|---|
task_type | TaskType.CAUSAL_LM | 模型类型:因果语言模型(自回归生成) |
target_modules | 7 个投影层 | 需要训练的层名:Attention 的q/k/v/o_proj与 MLP 的gate/up/down_proj,覆盖 LLaMA3.1 全部可注入的低秩适配层 |
r | 8 | LoRA 的秩,控制低秩矩阵的维度,秩越大可学习容量越大 |
lora_alpha | 32 | LoRA 缩放系数,实际缩放因子为lora_alpha / r = 4,用于调节低秩更新的强度 |
lora_dropout | 0.1 | 低秩分支的 Dropout 比例,用于缓解过拟合 |
inference_mode | False | 明确处于训练模式 |
get_peft_model会将原始模型包装为 PEFT 模型,model.print_trainable_parameters()会打印可训练参数量(LoRA 通常只占总参数的极小比例,这正是其高效的原因)。
⑤ TrainingArguments:训练策略
args = TrainingArguments( output_dir="./output/llama3_1_instruct_lora", per_device_train_batch_size=4, gradient_accumulation_steps=4, logging_steps=10, num_train_epochs=3, save_steps=100, # 为了快速演示,这里设置10,建议你设置成100 learning_rate=1e-4, save_on_each_node=True, gradient_checkpointing=True )output_dir:模型输出路径,checkpoint 将写入该目录;per_device_train_batch_size=4:单卡 batch size;gradient_accumulation_steps=4:梯度累积 4 步,等效 batch size 为 16,显存不足时减小 batch、增大累积即可;logging_steps=10:每 10 步输出一次日志;num_train_epochs=3:训练 3 个 epoch;save_steps=100:每 100 步保存一次 checkpoint;learning_rate=1e-4:学习率;gradient_checkpointing=True:以计算换显存,配合model.enable_input_require_grads()使用。
⑥ Trainer 训练
trainer = Trainer( model=model, args=args, train_dataset=tokenized_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) trainer.train() # 开始训练DataCollatorForSeq2Seq负责将 batch 内样本 padding 对齐(训练超长样本时也可考虑padding='longest'以节约显存);调用trainer.train()开始训练,训练参数中已设置自动保存策略,无需手动保存。整个训练过程大概需要20 ~ 30 分钟,完成后会在output目录下生成 LoRA 模型。
显存参考:本案例的 LLaMA3.1 基座模型为 8B 参数,配合 bfloat16 半精度与梯度检查点,24G 显存的 3090 级别机器即可胜任;在 self-llm 的 LLaMA3 Lora 微调教程 中同样以 24G 显存机器为推荐环境。
Step 4:推理测试:加载 LoRA 权重与嬛嬛对话
训练完成后,使用以下代码加载基座模型与 LoRA 权重进行对话测试:
from transformers import AutoModelForCausalLM, AutoTokenizer import torch from peft import PeftModel mode_path = './LLM-Research/Meta-Llama-3___1-8B-Instruct' lora_path = './output/llama3_1_instruct_lora/checkpoint-699' # 这里改称你的 lora 输出对应 checkpoint 地址 # 加载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 = "嬛嬛你怎么了,朕替你打抱不平!" messages = [ {"role": "system", "content": "假设你是皇帝身边的女人--甄嬛。"}, {"role": "user", "content": prompt} ] input_ids = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) # print(input_ids) model_inputs = tokenizer([input_ids], return_tensors="pt").to('cuda') generated_ids = model.generate(model_inputs.input_ids,max_new_tokens=512) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] print('皇上:', prompt) print('嬛嬛:',response)推理链路要点:
- 加载顺序:先用
AutoModelForCausalLM加载基座模型(bfloat16、device_map="auto"、.eval()切换评估模式),再用PeftModel.from_pretrained(model, model_id=lora_path)注入 LoRA 权重; lora_path指向训练输出的具体 checkpoint,例如./output/llama3_1_instruct_lora/checkpoint-699,请按实际保存步数替换;apply_chat_template直接复用 LLaMA3.1 的 chat template 组装system/user消息,add_generation_prompt=True会在末尾追加assistant引导符;model.generate(max_new_tokens=512)控制生成长度,随后按输入长度切片、skip_special_tokens=True解码,得到纯文本回复。
测试输出示例:
皇上: 嬛嬛你怎么了,朕替你打抱不平! 嬛嬛: 皇上,臣妾不是故意的。至此,一个可以流畅使用甄嬛语气对话的角色模型就训练并部署完成了。
扩展:换基座模型,复现同一流程
本案例以 LLaMA3.1-8B-Instruct 为基座完成了演示,但整套方法论完全可迁移:只需将 dataset/huanhuan.json(或自制的角色数据集)与 examples/Chat-嬛嬛/train.py 中的模型路径、对话模板替换为其他基座模型即可。self-llm 仓库中提供了大量同类 LoRA 微调教程可供参考,例如:
- LLaMA3-8B-Instruct Lora 微调:同样使用本案例的
huanhuan.json数据集,讲解指令集构建、数据格式化与训练全流程; - LLaMA3.1-8B-Instruct Lora 微调:与本案例基座一致,从环境配置到 LoRA 权重推理的完整参考。
值得注意的是,不同基座模型的 Chat Template 特殊 token 不同(如 Qwen 系列使用<|im_start|>/<|im_end|>,LLaMA3 系列使用<|start_header_id|>/<|eot_id|>),替换基座时需要同步修改process_func中的模板拼接,这也是将 train.py 迁移到其他模型时最需要关注的改动点。
写在最后
Chat-嬛嬛 诞生于大模型浪潮初期,项目团队抱着"如果不做点什么,可能会错过很多有趣的事情"的想法,与几位小伙伴一起完成了从剧本数据清洗、角色对话抽取、LoRA 微调到对话验证的完整闭环。整个过程既验证了"小说/剧本 → 角色 AI"这一个性化大模型路径的可行性,也为后来者提供了一份可以照抄的实战模板。如果你也有一部心仪的小说或剧本,不妨按照本文的四步流程,打造一个属于自己的角色 AI。
本案例的主要贡献者(Datawhale 成员):宋志学(中国矿业大学(北京))、邹雨衡(对外经济贸易大学)、王熠明(宁夏大学)、邓宇文(广州大学)、杜森(南阳理工学院)、肖鸿儒(同济大学)。
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考