返回博客列表

甄嬛模型微调

2026-01-29
4 min read
技术原理

一、微调(Finetuning)的原理 1. 什么是微调 微调就是: > 在已经训练好的大模型(比如 Qwen1-5B/8B)基础上,针对你的特定任务再训练,让模型学会你的“偏好”或“专用知识”。 类比: 大模型 = 一个通用的知识大脑(懂很多东西,但很笼统) 微调 = 在这个大脑上做“专项训练”,让它擅长你关心的领域(比如甄嬛聊天风格) --- 2. 为什么要微调 大模型很大:训练成本高,...

一、微调(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
peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj" ],
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
)
  • target_modules:只微调这些矩阵
  • r:低秩矩阵的秩(参数量大小)
  • lora_alpha:缩放系数
  • lora_dropout:训练中随机丢弃部分 LoRA 参数,防止过拟合

4. 微调步骤

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

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

    python
    encodings = tokenizer(text, truncation=True, max_length=512)
    example["input_ids"] = encodings["input_ids"]
    example["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
    trainer.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 总结流程

原始文本 -> 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
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
tokens = tokenizer(["Hello world!", "Hi"], padding=True, return_tensors="pt")
print(tokens["input_ids"])
print(tokens["attention_mask"])

输出可能是:

python
input_ids:
tensor([[15496,   995,     0],   # 0 是 padding
        [  72,     0,     0]])
attention_mask:
tensor([[1, 1, 0],
        [1, 0, 0]])

解释:

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

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

代码

python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
import pandas as pd
from datasets import Dataset
from trl import SFTTrainer
from transformers import TrainingArguments

device = "cpu"
print(f"Using device: {device}")

# 你当前环境不能加载 8B!只能换成小的 (1.8B 或 0.5B)
model_id = "./qw-chat05/Qwen/Qwen2___5-0___5B";

df = pd.read_json('../../dataset/huanhuan.json')
ds = Dataset.from_pandas(df)

tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

# CPU 模式:必须 float32
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    dtype=torch.float32,
    device_map={"": "cpu"},
    trust_remote_code=True
)

def build_text(example):
    messages = [
        {"role": "system", "content": "现在你要扮演皇帝身边的女人--甄嬛"},
        {"role": "user", "content": example["instruction"] + example["input"]},
        {"role": "assistant", "content": example["output"]},
    ]
    example["text"] = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=False
    )
    return example

ds = ds.map(build_text)

peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
)


# ------------------------------------------------------
# 5. 训练参数
# ------------------------------------------------------
training_args = TrainingArguments(
    output_dir="./qwen-huanhuan-lora",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=8,
    learning_rate=2e-4,
    num_train_epochs=3,
    logging_steps=5,
    save_strategy="epoch",
    fp16=False,
    bf16=False,
)

# ------------------------------------------------------
# 6. SFTTrainer(注意:TRL 0.26 不接受 formatting_func)
# ------------------------------------------------------
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=ds,
    peft_config=peft_config,
)
trainer.train()
trainer.save_model("./qwen-huanhuan-lora2/final")

模型推理

python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

# 强制使用 CPU(避免 MPS 兼容性问题)

device = "cpu"
print(f"Using device: {device}")
model_id = "./qw-chat05/Qwen/Qwen2___5-0___5B";

lora_path = "./qwen-huanhuan-lora/final"
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)

base_model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map={"": device},
    trust_remote_code=True
)

# 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, lora_path, device_map={"": device})
# 推理示例
questions = ['你是谁,你生日是哪一天']
for q in questions:
    inputs = tokenizer.apply_chat_template([
        {"role": "user", "content": q}
    ], return_tensors="pt", return_dict=True)
    inputs = {k: v.to(device) for k, v in inputs.items()}
    outputs = model.generate(**inputs, max_new_tokens=128, do_sample=True,
                             top_p=0.9)
    print(tokenizer.decode(outputs[0]))

日志

{'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%,还有提升空间

返回博客列表
最后更新于 2026-01-29
想法或问题?在 GitHub Issue 下方参与讨论
去评论