前言

在上一篇文章《【AI学习-2.1】部署自己的本地大模型 —— 本地推理》中,我们已经完成了一件非常关键的事情:
在本地,把一个真实的大模型完整地跑了起来,但是在这一阶段,我们更多是在“使用模型”。 但如果模型永远只是一个只能调用、却无法改变的黑盒,那么对 AI 的理解始终停留在表层。这一篇,我们参数训练模型,改变原有的模型


一、什么是模型微调

微调并不是“重新训练一个模型”。 而是在原有模型的基础上做一些调整,改变他的参数,让他往你要的方向靠近

以 Qwen3-4B 为例,它已经在海量语料上完成过完整训练,内部参数规模达到数十亿级别。如果我们对它进行全参数训练:

  • 对硬件资源要求极高
  • 训练时间非常长
  • 学习和试错成本都难以接受

因此,在实际工程和学习过程中,更常见的做法是:
在不破坏原有模型能力的前提下,对模型进行小幅度、定向的调整。

这类方法统称为参数高效微调(PEFT),而 LoRA 是其中最常见、也最成熟的一种方案。


二、什么是 LoRA,为什么从它开始

LoRA 的全称是 Low-Rank Adaptation,中文通常翻译为“低秩适配”。

从思想上看,LoRA 并不会直接修改原始模型的参数,而是:

  • 冻结原模型的全部权重
  • 在模型的关键线性层中插入一小组可训练参数
  • 训练时只更新这些新增参数

可以把它理解为: 原模型是一台已经调校完成的精密机器,而 LoRA 是在关键位置外挂的一组“微调旋钮”。

LoRA 的主要特点

  • 不改变原始模型参数
  • 新增参数量极小,通常只占原模型参数的极小一部分
  • 显存和内存占用低
  • 训练完成后可以随时加载或卸载

LoRA 常见使用场景

  • 学习和理解大模型微调流程
  • 针对特定任务或风格进行定向训练
  • 单卡、小显存或资源受限的本地环境

正因为这些特性,LoRA 非常适合作为第一次接触模型微调的起点。


三、本篇文章要做什么

在这一篇中,我们将完成一次完整、但刻意“简化”的本地微调流程:

  1. 加载已经训练好的基础模型
  2. 使用 LoRA 注入可训练参数
  3. 加载一份监督微调数据
  4. 执行训练
  5. 保存 LoRA 权重

整个过程的目标不是训练效果,而是理解每一步在做什么,以及为什么要这么做。


四、基础训练参数的含义

在微调中,这些参数经常会让新手感到困惑,这里逐一解释。

max_length = 512
lr = 2e-4
epochs = 2
batch_size = 1

batch_size

batch_size 表示:
每一次参数更新时,模型同时“看到”的样本数量。

可以把训练过程理解为一个循环:
模型先读取一小批数据,计算误差(loss),然后根据误差对参数进行一次更新。
这一小批数据的数量,就是 batch_size。

  • batch_size 越大,梯度估计越稳定
  • batch_size 越大,占用的显存或内存也越多

在本地微调大模型时,最常见的问题并不是训练慢,而是模型根本跑不起来,直接因为内存或显存不足而中断。

因此在学习阶段,将 batch_size 设置为 1,是一个非常稳妥的选择。
它的核心目标不是效率,而是优先保证训练流程能够完整跑通。

learning_rate

learning_rate(学习率)控制的是模型参数每一次更新时,调整的幅度大小。

可以简单理解为:
模型“犯一次错”,要改正多少。

  • 学习率过大:参数更新过猛,训练容易震荡甚至发散
  • 学习率过小:训练非常稳定,但学习速度极慢

在 LoRA 微调中,由于可训练参数本身就很少,学习率通常会比全参数训练略大一些。
2e-4 是一个非常常见、也相对安全的起始值。

epochs

epoch 表示:
整个训练数据集被模型完整“看一遍”的次数。

例如:

  • epochs = 1:(平均)每条数据只参与一次训练
  • epochs = 2:(平均)每条数据会被模型看到两次

在本文中,我们并不追求模型效果,而是希望完整跑通一次微调流程,因此只设置为 2。


五、Tokenizer 在微调中的作用

Tokenizer 的作用,是把人类可读的文本转换为模型可以处理的 token 序列。

在因果语言模型中,经常会遇到一个实际问题:
模型没有显式定义 pad_token,但训练时又需要对不同长度的样本进行 padding。

if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

将 pad_token 设置为 eos_token,是一种非常常见且安全的做法,可以避免训练过程中的告警和错误。


六、加载基础模型

model = AutoModelForCausalLM.from_pretrained(
    model_path,
    device_map="auto",
    trust_remote_code=True
)

这一步与上一篇文章中的推理加载非常相似。
不同的是,此时模型将进入训练流程,但原始权重随后会被 LoRA 冻结,不会发生更新。


七、LoRA 配置的含义

lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

这部分是 LoRA 微调的核心配置。

  • target_modules 指定 LoRA 注入的位置
    在这里,我们只对注意力机制中的 q_proj 和 v_proj 生效
  • r 决定了 LoRA 的低秩维度,数值越大,表达能力越强
  • lora_alpha 用于控制 LoRA 更新的整体幅度
  • lora_dropout 用于缓解过拟合

从经验上看,将 LoRA 注入到注意力层,通常可以在极小参数量的前提下,显著影响模型行为。


八、加载监督微调数据

dataset = load_dataset("json", data_files=dataset_path, split="train")

这里使用的是 jsonl 格式的数据集。

在监督微调(SFT)中,每一条样本通常包含:

  • 输入(用户问题或指令)
  • 输出(期望模型给出的回答)

SFTTrainer 会自动完成样本拼接、tokenize、mask 构造和 loss 计算。


九、训练参数与 Trainer

training_args = TrainingArguments(
    output_dir=r"E:\it\project\pycharm\learnModel\lora\first",
    per_device_train_batch_size=batch_size,
    learning_rate=lr,
    num_train_epochs=epochs,
    logging_steps=10,
    save_strategy="epoch",
    save_total_limit=2,
    fp16=torch.cuda.is_available(),
    optim="adamw_torch"
)

output_dir 用来指定训练过程中产生的所有文件保存到哪里,包括 LoRA 权重和中间的 checkpoint。训练结束后真正有用的结果基本都在这个目录里。

per_device_train_batch_size 表示每张卡(或 CPU)一次喂给模型多少条数据。这个值越大,占用的显存或内存越多,本地学习阶段一般都设得很小。

learning_rate 控制模型参数每一步更新的幅度大小。太大会导致训练不稳定,太小又学得很慢,这里直接复用前面定义好的学习率。

num_train_epochs 表示整个数据集会被模型完整训练多少轮。学习阶段不追求效果,跑一两轮主要是为了走通流程。

logging_steps 用来控制每隔多少步打印一次训练日志。值设小一点,可以更直观地看到 loss 是否在下降。

save_strategy 决定什么时候保存模型,这里设置为 “epoch”,表示每跑完一轮数据就保存一次。这样比较直观,也方便之后回滚或对比。

save_total_limit 用来限制最多保留多少个保存点,防止 checkpoint 越存越多把磁盘占满。超过这个数量,旧的 checkpoint 会被自动删除。

fp16 表示是否启用半精度训练,这里只有在 GPU 可用时才开启。开启后可以明显降低显存占用,对本地训练很友好。

optim 用来指定使用哪种优化器,adamw_torch 是一个比较稳妥、默认推荐的选择。对于 LoRA 这种微调场景,一般不需要纠结优化器类型。


十、开始训练并保存 LoRA 权重

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    peft_config=lora_config,
    processing_class=tokenizer,
    args=training_args
)

trainer.train()
trainer.save_model(training_args.output_dir)

训练结束后,保存下来的并不是完整模型,而只是 LoRA 权重和相关配置文件。

这意味着:

  • 原始模型可以被多个 LoRA 复用
  • 不同任务只需要切换不同的 LoRA 权重
  • 微调成本和风险都被大幅降低

十一、开始微调

自此,我们已经介绍微调的一些基础概念和步骤,总体代码如下

import torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments
from peft import LoraConfig
from trl import SFTTrainer

# =========================
# 一、基础配置
# =========================
#换成你自己Qwen3-4B代码的位置
model_path = r"E:\it\project\pycharm\Qwen3-4B"
#换成你自己训练集的位置
dataset_path = r"E:\it\project\pycharm\learnModel\resource\finetune_data.jsonl"
device = "cuda" if torch.cuda.is_available() else "cpu"
max_length = 512
lr = 2e-4
epochs = 2
batch_size = 1

# =========================
# 二、Tokenizer
# =========================
tokenizer = AutoTokenizer.from_pretrained(
    model_path,
    trust_remote_code=True
)

# 避免告警
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

# =========================
# 三、加载基础模型
# =========================
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    device_map="auto",
    trust_remote_code=True
)

# =========================
# 四、LoRA 配置
# =========================
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

# =========================
# 五、加载数据集并 tokenize
# =========================
dataset = load_dataset("json", data_files=dataset_path, split="train")



# =========================
# 训练参数
# =========================
training_args = TrainingArguments(
    output_dir=r"E:\it\project\pycharm\learnModel\lora\first",
    per_device_train_batch_size=batch_size,
    learning_rate=lr,
    num_train_epochs=epochs,
    logging_steps=10,
    save_strategy="epoch",
    save_total_limit=2,
    fp16=torch.cuda.is_available(),  # GPU 支持时开启半精度
    optim="adamw_torch"
)

# =========================
# SFTTrainer 初始化
# =========================
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    peft_config=lora_config,
    processing_class=tokenizer,
    args=training_args
)

# =========================
# 八、开始训练
# =========================
trainer.train()

# =========================
# 九、保存 LoRA 权重
# =========================
trainer.save_model(training_args.output_dir)

dataset_path 就是是你的训练数据文件,为了训练得快,我随便写了两行,实际上训练数据会很多

{"messages":[{"role":"user","content":"Kafka 是什么?"},{"role":"assistant","content":"Kafka 是一个分布式消息流平台。"}]}
{"messages":[{"role":"user","content":"Corki Tse 是谁?"},{"role":"assistant","content":"Corki Tse 是一个歌手,出生于中国,代表作是《VIVI》"}]}

运行后你会得到一个文件,就放在你output_dir指定的位置上,这个就是我们微调的成果,这里并不会保存原模型的全部参数,而是保存训练后的增量变化部分
在这里插入图片描述

十一、使用微调的模型后做推理

在上一章推理代码上,替换下原本的model就可以了

model_path = r"E:\it\project\pycharm\Qwen3-4B"
#这里就是刚才微调时output_dir指定的文件
lora_path = r"E:\it\project\pycharm\learnModel\lora\first"

#获取到微调后的tokenizer
tokenizer = AutoTokenizer.from_pretrained(
    lora_path,
    trust_remote_code=True
)

# 原来的大模型
base_model = AutoModelForCausalLM.from_pretrained(
    model_path,
    device_map="cpu",
    dtype=torch.float32,
    trust_remote_code=True,
    # 启用分块加载 + 就地绑定, 在加载模型权重时,避免“权重在内存里被复制一遍”,把“加载峰值内存”降到最低。
    low_cpu_mem_usage=True
)

#在原来的大模型原来大模型的基础上加上刚才lora的训练结果
model = PeftModel.from_pretrained(
    base_model,
    lora_path
)

#... 接下来就是原本的逻辑

执行过程和结果

在这里插入图片描述
在这里插入图片描述

虽然感觉训练和没训练一样,但是这也正常,我们本身的微调训练数据就太少了,我们本次学习知识掌握怎么训练,后面需要自己训练得时候除了需要准备很多微调数据,还有很多其他参数也需要根据自己需求优化。前面的文章知识让大家对于模型有个初体验,拉进我们和ai的距离

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐