项目概况

甄嬛模型微调

2026-01-294 min read未分类
技术原理

一、微调(Fine-tuning)的原理

1. 什么是微调

微调就是:

在已经训练好的大模型(比如 Qwen1-5B/8B)基础上,针对你的特定任务再训练,让模型学会你的“偏好”或“专用知识”。

类比:

  • 大模型 = 一个通用的知识大脑(懂很多东西,但很笼统)
  • 微调 = 在这个大脑上做“专项训练”,让它擅长你关心的领域(比如甄嬛聊天风格)

2. 为什么要微调

  • 大模型很大:训练成本高,普通人无法从零训练。
  • 直接用大模型:模型可能回答通用,但不符合你的风格或任务。
  • 微调:只改变部分参数(LoRA)就能适配任务,不会更新原模型的所有权重,成本低、速度快。

3. 微调原理细节

大模型有大量参数(比如 80 亿个参数):

  • 全量微调:修改模型所有参数(消耗大、容易过拟合)
  • LoRA 微调:只修改部分矩阵的低秩参数(低成本、高效)

大模型里的一个线性层原来是:$$y = Wx$$ LoRA 方法是:$$y = Wx + BAx$$

其中: W 不变(老师原有知识) A、B 是新增的小矩阵(老师的小笔记本) 你只训练 A 和 B(非常省显存) 这就相当于: 主大脑(W)不改 学新技能用“外挂笔记”(BA)

LoRA

python
1peft_config = LoraConfig( 2 task_type=TaskType.CAUSAL_LM, 3 target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj" ], 4 r=8, 5 lora_alpha=16, 6 lora_dropout=0.05, 7)
  • target_modules:只微调这些矩阵
  • r:低秩矩阵的秩(参数量大小)
  • lora_alpha:缩放系数
  • lora_dropout:训练中随机丢弃部分 LoRA 参数,防止过拟合

4. 微调步骤

  1. 准备数据 你的 JSON 数据,每条包含:

    • instruction(用户问题)
    • input(可选上下文)
    • output(模型应答)
  2. 数据处理(tokenizer) 模型只能处理数字(token),所以要把文本转成 input_idsattention_mask

    python
    1encodings = tokenizer(text, truncation=True, max_length=512) 2example["input_ids"] = encodings["input_ids"] 3example["attention_mask"] = encodings["attention_mask"]
  3. 定义 LoRA 配置 告诉模型只微调哪些参数。

  4. 定义训练参数TrainingArguments

    • per_device_train_batch_size:一次喂多少条数据
    • gradient_accumulation_steps:梯度累积(一次更新前累积多少步)
    • learning_rate:学习率
    • num_train_epochs:训练轮次
    • fp16/bf16:混合精度(CPU 上关闭)
  5. 训练(SFTTrainer) 把模型、数据、训练参数、LoRA 配置交给 SFTTrainer,调用 .train() 就开始训练。

  6. 保存模型 训练完成后保存:

    python
    1trainer.save_model("./qwen-huanhuan-lora/final")

二、代码里 API 的作用

API / 类功能
AutoTokenizer.from_pretrained(model_id)加载模型的分词器,把文字变成数字序列(token)
AutoModelForCausalLM.from_pretrained(model_id)加载预训练的大语言模型
LoraConfig配置 LoRA 微调参数(只修改部分矩阵)
SFTTrainer封装训练逻辑,包括前向传播、反向传播、梯度更新
TrainingArguments训练参数,控制训练细节(batch、lr、轮次等)
Dataset.from_pandas(df)把 pandas 数据转换成 HuggingFace 数据集格式
tokenizer(...)将文本转成模型能理解的 token ID,并生成 attention mask
dataset.map(preprocess)对每条数据应用 preprocess 函数,把文本转换成 input_ids

2.1 专业名词

  • 前向传播

意思:把输入数据“喂给模型”,让模型算出预测结果。 类比:你在做一道题,先写出你的答案。

  • 后向传播

意思:计算模型预测结果和真实答案的差距,然后算出每个参数对这个差距的贡献。 类比:你检查答案错了多少,并想:“哪些步骤做错了?怎么改?”

  • 梯度更新

意思:用反向传播算出来的梯度,调整模型参数,让模型更准确。 类比:你根据检查的错误修改解题方法,下次做题更对。

2.2 CPU 微调注意事项

  • 模型大(1B+)在 CPU 上微调很慢
  • 要关闭 fp16 / bf16
  • LoRA 微调可以大幅降低显存和计算需求
  • 对于小模型(0.5B ~ 1.8B)在 CPU 上才比较可行

2.3 总结流程

text
1原始文本 -> tokenizer -> input_ids -> SFTTrainer(模型+LoRA+训练参数+数据) -> train -> 保存模型

微调实质就是在已有大模型上,对特定任务的数据做梯度更新,只改一小部分参数

二、实战展示

数据集准备

模型微调

attention_mask

attention_mask 是 Transformer 模型(如 BERT、GPT 系列)中非常重要的一个张量,用来 告诉模型哪些 token 需要注意,哪些 token 是填充(padding)无需关注的。

在 NLP 模型中,为了让批量训练更高效,通常需要 把句子补齐到相同长度,这时就会产生 padding token。如果模型把 padding token 当作有效信息,会影响输出结果。 attention_mask 就是用来标记这些 padding token 的。

python
1from transformers import AutoTokenizer 2tokenizer = AutoTokenizer.from_pretrained("gpt2") 3tokens = tokenizer(["Hello world!", "Hi"], padding=True, return_tensors="pt") 4print(tokens["input_ids"]) 5print(tokens["attention_mask"]) 6

输出可能是:

python
1input_ids: 2tensor([[15496, 995, 0], # 0 是 padding 3 [ 72, 0, 0]]) 4attention_mask: 5tensor([[1, 1, 0], 6 [1, 0, 0]])

解释:

  • 第一句 "Hello world!" 长度 2,补了一个 padding → attention_mask = [1,1,0]
  • 第二句 "Hi" 长度 1,补了两个 padding → attention_mask = [1,0,0]

模型在计算 self-attention 时会 忽略 padding 的位置。

代码

python
1import torch 2from transformers import AutoModelForCausalLM, AutoTokenizer 3from peft import LoraConfig, get_peft_model, TaskType 4import pandas as pd 5from datasets import Dataset 6from trl import SFTTrainer 7from transformers import TrainingArguments 8 9device = "cpu" 10print(f"Using device: {device}") 11 12# 你当前环境不能加载 8B!只能换成小的 (1.8B 或 0.5B) 13model_id = "./qw-chat05/Qwen/Qwen2___5-0___5B"; 14 15df = pd.read_json('../../dataset/huanhuan.json') 16ds = Dataset.from_pandas(df) 17 18tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) 19tokenizer.pad_token = tokenizer.eos_token 20tokenizer.padding_side = "right" 21 22# CPU 模式:必须 float32 23model = AutoModelForCausalLM.from_pretrained( 24 model_id, 25 dtype=torch.float32, 26 device_map={"": "cpu"}, 27 trust_remote_code=True 28) 29 30def build_text(example): 31 messages = [ 32 {"role": "system", "content": "现在你要扮演皇帝身边的女人--甄嬛"}, 33 {"role": "user", "content": example["instruction"] + example["input"]}, 34 {"role": "assistant", "content": example["output"]}, 35 ] 36 example["text"] = tokenizer.apply_chat_template( 37 messages, 38 tokenize=False, 39 add_generation_prompt=False 40 ) 41 return example 42 43ds = ds.map(build_text) 44 45peft_config = LoraConfig( 46 task_type=TaskType.CAUSAL_LM, 47 target_modules=[ 48 "q_proj", "k_proj", "v_proj", "o_proj", 49 "gate_proj", "up_proj", "down_proj" 50 ], 51 r=8, 52 lora_alpha=16, 53 lora_dropout=0.05, 54) 55 56 57# ------------------------------------------------------ 58# 5. 训练参数 59# ------------------------------------------------------ 60training_args = TrainingArguments( 61 output_dir="./qwen-huanhuan-lora", 62 per_device_train_batch_size=1, 63 gradient_accumulation_steps=8, 64 learning_rate=2e-4, 65 num_train_epochs=3, 66 logging_steps=5, 67 save_strategy="epoch", 68 fp16=False, 69 bf16=False, 70) 71 72# ------------------------------------------------------ 73# 6. SFTTrainer(注意:TRL 0.26 不接受 formatting_func) 74# ------------------------------------------------------ 75trainer = SFTTrainer( 76 model=model, 77 args=training_args, 78 train_dataset=ds, 79 peft_config=peft_config, 80) 81trainer.train() 82trainer.save_model("./qwen-huanhuan-lora2/final") 83

模型推理

python
1import torch 2from transformers import AutoModelForCausalLM, AutoTokenizer 3from peft import PeftModel 4 5# 强制使用 CPU(避免 MPS 兼容性问题) 6 7device = "cpu" 8print(f"Using device: {device}") 9model_id = "./qw-chat05/Qwen/Qwen2___5-0___5B"; 10 11lora_path = "./qwen-huanhuan-lora/final" 12tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) 13 14base_model = AutoModelForCausalLM.from_pretrained( 15 model_id, 16 device_map={"": device}, 17 trust_remote_code=True 18) 19 20# 加载 LoRA 权重 21model = PeftModel.from_pretrained(base_model, lora_path, device_map={"": device}) 22# 推理示例 23questions = ['你是谁,你生日是哪一天'] 24for q in questions: 25 inputs = tokenizer.apply_chat_template([ 26 {"role": "user", "content": q} 27 ], return_tensors="pt", return_dict=True) 28 inputs = {k: v.to(device) for k, v in inputs.items()} 29 outputs = model.generate(**inputs, max_new_tokens=128, do_sample=True, 30 top_p=0.9) 31 print(tokenizer.decode(outputs[0])) 32

日志

text
1{'loss': 3.06, 'grad_norm': 1.6156032085418701, 'learning_rate': 9.336188436830836e-05, 'entropy': 3.06442369222641, 'num_tokens': 137673.0, 'mean_token_accuracy': 0.5385425604879857, 'epoch': 0.54}

这条日志:

训练还不到一轮(epoch 0.54) 当前 loss = 3.06,还在下降阶段 梯度正常(grad_norm ≈ 1.6) 学习率约 0.000093 模型 token-level 正确率 ≈ 53.85%,还有提升空间