从零开始:Qwen3.5-4B 大模型 LoRA 微调实战教程

本文手把手带你完成大模型微调全流程:模型下载 → 数据准备 → LoRA 微调 → 推理测试。零基础也能看懂!


目录

  1. 项目背景
  2. 环境准备
  3. 第一步:下载基础模型
  4. 第二步:准备训练数据
  5. 第三步:数据预处理(Tokenization)
  6. 第四步:LoRA 微调训练
  7. 第五步:加载微调模型并推理
  8. 完整项目文件结构
  9. 常见问题(FAQ)

1. 项目背景

我们要做什么?

简历评分数据来微调 Qwen3.5-4B 大模型,让它学会根据工作经历给出专业的评分和建议。

什么是大模型微调?

想象一个场景:你有一个很聪明的大学毕业生(基础模型),他什么都懂一点,但遇到专业问题(比如给简历打分)就不够精准。微调就是给这个毕业生做"岗前培训"——用专业数据训练他,让他成为某个领域的专家。

什么是 LoRA?

LoRA(Low-Rank Adaptation,低秩适配) 是一种高效的微调技术。打个比方:

  • 全量微调:把整本书每个字都重新写一遍(消耗巨大)
  • LoRA 微调:只在不重要的页码上贴便利贴做笔记(轻量高效)

LoRA 的优点是:

  • 训练速度快
  • 显存占用低(本文的 4B 模型,10GB 左右显存就能跑)
  • 微调后的"增量文件"只有几 MB(原始模型 8GB+)

硬件要求

配置 最低要求 推荐配置
GPU 显存 8GB 16GB+
内存 16GB 32GB
硬盘 20GB 50GB

2. 环境准备

安装必要的 Python 库

pip install torch transformers datasets peft modelscope swanlab

各库的作用:

库名 作用
torch PyTorch 深度学习框架
transformers HuggingFace 模型库,加载 Qwen 模型
datasets 数据处理工具
peft LoRA 微调的核心库
modelscope 国内模型下载(魔搭社区)
swanlab 训练过程可视化工具(可选)

3. 第一步:下载基础模型

对应文件:model_download.py

from modelscope import snapshot_download

model_dir = snapshot_download("Qwen/Qwen3.5-4B", cache_dir='./模型微调')
print(f"模型下载完毕,保存位置在{model_dir}")

代码解读

  1. snapshot_download 是 ModelScope(魔搭社区)提供的下载工具
  2. "Qwen/Qwen3.5-4B" 是模型名称——Qwen3.5 系列,参数量 40 亿(4B)
  3. cache_dir='./模型微调' 指定下载到哪个文件夹
  4. 下载完成后,模型会保存在 ./模型微调/Qwen/Qwen3___5-4B/ 目录下

下载了什么?

Qwen3___5-4B/
├── config.json              # 模型配置(多少层、隐藏层大小等)
├── model.safetensors-xxx    # 模型权重文件(约 8GB)
├── tokenizer.json           # 分词器
├── tokenizer_config.json    # 分词器配置
├── chat_template.jinja      # 对话模板
├── vocab.json / merges.txt  # 词表文件
└── ...

注意.safetensors 是一种安全的模型存储格式,比旧的 .bin 格式更快更安全。


4. 第二步:准备训练数据

数据格式

我们的训练数据是 JSON 格式,每行一条记录,放在 data/llm_resume_scoring.json 中:

{
  "question": {
    "工作时间": "2015.06-2020.12",
    "工作内容": "1、负责公司ERP系统的需求分析和功能设计...",
    "职务": "IT项目经理",
    "工作单位": "上海信息科技有限公司"
  },
  "answer": "技术能力:8分, 解释:候选人成功完成了ERP系统需求分析..."
}

数据解读

每条数据包含两部分:

  • question(问题):一份工作经历,包含时间、工作内容、职务、工作单位
  • answer(答案):对该工作经历的评分和详细评价

这就是"问答对"格式——相当于我们给模型看"标准答案",让它学会如何评分。

数据预处理代码

对应文件:p2.py

import json
from datasets import Dataset
from transformers import AutoTokenizer

# 1. 加载分词器
tokenizer = AutoTokenizer.from_pretrained('./模型微调/Qwen/Qwen3___5-4B')

# 2. 读取数据
data = []
with open("./data/llm_resume_scoring.json", "r", encoding="UTF-8") as f:
    for line in f:
        line = line.strip()
        if line:
            data.append(json.loads(line))

# 3. 转为 Dataset 格式
ds = Dataset.from_list(data)

什么是 Tokenization?

模型不能直接理解中文文字,需要把文字转换成数字——这个过程叫 Tokenization(分词编码)

"我是程序员"  →  [1234, 5678, 9012]   # 每个数字代表一个 token

处理函数详解

def process_func(example):
    MAX_LENGTH = 512
    SYS = "给定一个候选人的工作经历信息,你需要针对每个职位进行综合评分..."

    messages = [
        {"role": "system", "content": SYS},
        {"role": "user", "content": json.dumps(example["question"], ensure_ascii=False)},
        {"role": "assistant", "content": json.dumps(example["answer"], ensure_ascii=False)}
    ]

    # prompt 部分 = system + user(模型应该看到的输入)
    prompt_ids = tokenizer.apply_chat_template(
        messages[:2], tokenize=True, add_generation_prompt=True
    )

    # full = system + user + assistant(完整的对话)
    full_ids = tokenizer.apply_chat_template(
        messages, tokenize=True, add_generation_prompt=False
    )

    # response_ids = assistant 的回答部分
    response_ids = full_ids[len(prompt_ids):]

    # labels:prompt 部分设为 -100(不计算损失),只对回答部分计算损失
    input_ids = prompt_ids + response_ids
    labels = [-100] * len(prompt_ids) + response_ids

    # 截断到最大长度
    if len(input_ids) > MAX_LENGTH:
        input_ids = input_ids[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]

    return {
        "input_ids": input_ids,
        "attention_mask": [1] * len(input_ids),
        "labels": labels
    }

关键概念——为什么 prompt 的 label 设为 -100?

在训练大模型时,labels 控制模型在哪些位置"学习":

输入部分(system + user)→ labels = -100  → 不学习(只是上下文)
输出部分(assistant)   → labels = 真实值 → 学习(模型要预测这些)

就像老师告诉你:“前面的话是题目,最后那句话才是你要学会写的答案。”


5. 第三步:数据预处理(Tokenization)

对应文件:p2.py(与上一步合并)

# 对所有数据进行 tokenization
tokenized_id = ds.map(process_func, remove_columns=ds.column_names)

# 查看处理后第一条数据的实际内容
print(tokenizer.decode(tokenized_id[0]["input_ids"]))

执行 ds.map(process_func) 会让 process_func 函数对每一条数据都执行一遍,把原始文本全部转换成数字 ID。


6. 第四步:LoRA 微调训练

对应文件:p3.py

这是整个项目最核心的部分。我们来看完整的训练代码:

6.1 加载基础模型

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained(
    './模型微调/Qwen/Qwen3___5-4B',
    dtype=torch.bfloat16,    # 使用 bfloat16 精度,节省一半显存
    device_map="cuda",       # 自动分配到 GPU
)

tokenizer = AutoTokenizer.from_pretrained('./模型微调/Qwen/Qwen3___5-4B')

知识点

  • bfloat16:16 位浮点数格式,比 32 位节省一半显存,精度损失很小
  • device_map="cuda":把模型放到 GPU 上,推理速度提升 10-100 倍

6.2 配置 LoRA

from peft import LoraConfig, TaskType, get_peft_model

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,    # 因果语言模型任务
    target_modules=[                 # 要对哪些层做 LoRA
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    inference_mode=False,            # 训练模式
    r=8,                             # LoRA 秩(rank)
    lora_alpha=32,                   # LoRA 缩放系数
    lora_dropout=0.1,                # Dropout 防止过拟合
)

model = get_peft_model(model, config)
model.print_trainable_parameters()

LoRA 参数详解

参数 含义 建议值
r LoRA 的秩,越大能力越强,但也越慢 4~16
lora_alpha 缩放系数,通常是 r 的 2~4 倍 16~64
lora_dropout 随机丢弃比例,防止过拟合 0.05~0.1
target_modules 要微调的层,Qwen 模型用这 7 个 全选

LoRA 的工作原理(简单版):

模型中的权重矩阵是 4096×4096 的大矩阵,全量微调要更新所有 4096×4096=1600 万个参数。LoRA 把这个大更新拆成两个小矩阵的乘积(比如 4096×8 和 8×4096),只需要更新 4096×8×2=65000 个参数——相当于只更新原来的 0.4%

6.3 配置训练参数

from transformers import TrainingArguments

args = TrainingArguments(
    output_dir="./output/Qwen3_5_LoRA",     # 训练结果保存位置
    per_device_train_batch_size=4,          # 每 GPU 批次大小
    gradient_accumulation_steps=6,          # 梯度累积(等效 batch_size=24)
    gradient_checkpointing=True,            # 节省显存
    logging_steps=10,                       # 每 10 步打印日志
    num_train_epochs=3,                     # 训练 3 轮
    save_steps=100,                         # 每 100 步保存一次
    learning_rate=1e-4,                     # 学习率
    bf16=True,                              # bfloat16 加速
    report_to="none",                       # 不上报训练日志
)

训练参数详解

参数 含义 为什么这样设?
per_device_train_batch_size=4 每步处理 4 条数据 显存有限,不能太大
gradient_accumulation_steps=6 累积 6 步再更新 等效 batch_size=4×6=24,训练更稳定
gradient_checkpointing=True 用时间换空间 可以节省约 40% 显存
num_train_epochs=3 重复训练 3 遍 太少没学会,太多会"背答案"
learning_rate=1e-4 学习率 0.0001 LoRA 常用值,收敛平稳
save_steps=100 每 100 步存一次 可以随时恢复训练

6.4 开始训练

from transformers import Trainer, DataCollatorForSeq2Seq

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

trainer.train()

Trainer 是 HuggingFace 提供的"一键训练器",它会自动处理:

  • 前向传播(模型计算预测结果)
  • 计算损失(预测与实际差多少)
  • 反向传播(根据差距调整参数)
  • 梯度累积和优化

6.5 训练完成后

训练完成后,output/Qwen3_5_LoRA/checkpoint-198/ 目录下会出现:

checkpoint-198/
├── adapter_config.json        # LoRA 配置
├── adapter_model.safetensors  # LoRA 增量权重(几 MB)
├── optimizer.pt               # 优化器状态
├── trainer_state.json         # 训练状态
└── ...

最关键的只有两个文件:adapter_config.jsonadapter_model.safetensors,加起来通常只有几 MB。


7. 第五步:加载微调模型并推理

对应文件:p4.py

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

# 1. 加载基础模型和分词器
model_id = "./模型微调/Qwen/Qwen3___5-4B"
lora_path = "./output/Qwen3_5_LoRA/checkpoint-198"

tokenizer = AutoTokenizer.from_pretrained(model_id)

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    dtype=torch.bfloat16,
    device_map="cuda"
)

# 2. 加载 LoRA 增量权重
model = PeftModel.from_pretrained(model, model_id=lora_path)
model.eval()

# 3. 构造测试问题
messages = [
    {"role": "system", "content": "你是一个专业的HR面试官,请根据候选人的工作经历给出客观评分和详细建议。"},
    {"role": "user", "content": "候选人工作经历:工作时间 2015.06-2020.12,职务 IT项目经理..."}
]

# 4. 编码输入
inputs = tokenizer.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=True,
    return_tensors="pt",
).to(model.device)

# 5. 生成回复
gen_kwargs = {
    "max_new_tokens": 1024,
    "do_sample": True,
    "top_p": 0.8,
    "temperature": 0.7
}

with torch.no_grad():
    outputs = model.generate(**inputs, **gen_kwargs)

# 6. 解码输出
outputs = outputs[:, inputs["input_ids"].shape[1]:]
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

代码解读

加载 LoRA 权重

model = PeftModel.from_pretrained(model, model_id=lora_path)

这一行把基础模型 + LoRA 增量"组装"在一起,形成微调后的完整模型。

生成参数

参数 含义 效果
max_new_tokens=1024 最多生成 1024 个 token 约 2000 个汉字
do_sample=True 随机采样 不会每次生成一样
top_p=0.8 核采样,只从概率前 80% 的词中选 平衡创意与准确
temperature=0.7 温度越高越随机 0.7 是比较均衡的值

8. 完整项目文件结构

模型微调/
├── model_download.py           # 下载 Qwen3.5-4B 基础模型
├── p1.py                       # 测试基础模型(模板输出)
├── p2.py                       # 数据预处理(tokenization)
├── p3.py                       # LoRA 微调训练
├── p4.py                       # 加载微调模型并推理
├── data/
│   └── llm_resume_scoring.json # 训练数据(简历评分问答对)
├── output/
│   └── Qwen3_5_LoRA/
│       └── checkpoint-198/     # 微调结果(LoRA 增量权重)
└── 模型微调/
    └── Qwen/Qwen3___5-4B/      # 下载的基础模型

执行顺序

model_download.py
下载模型

p2.py
数据预处理

p3.py
LoRA训练

p4.py
推理测试

  1. 先跑 model_download.py:把 Qwen3.5-4B 下载到本地
  2. 再跑 p2.py:把 JSON 数据转换成模型能理解的 token 格式(验证数据正确性)
  3. 然后跑 p3.py:开始 LoRA 微调训练(核心步骤,耗时最长)
  4. 最后跑 p4.py:用微调好的模型进行推理测试

9. 常见问题(FAQ)

Q1:为什么选择 Qwen3.5-4B 而不是更大的模型?

4B(40 亿参数)是一个很好的"入门尺寸":

  • 单张 16GB 显卡就能训练
  • 效果对于简历评分这类任务已经足够
  • 训练一个 epoch 大约 10-20 分钟

如果你想挑战更大的模型(如 7B、14B),只需要把模型名称改一下,其他代码基本不用动。

Q2:训练多久合适?

本文设置 num_train_epochs=3(3 轮),对于 50 条数据、batch_size=24 的情况:

  • 每轮约 2-3 个 step
  • 3 轮总共约 6-9 个 step
  • 总耗时约 5-15 分钟(取决于显卡)

如果数据量更大(几百上千条),可以适当增加 epoch 数,但要监控过拟合(模型只记住了训练数据,遇到新问题不会答)。

Q3:显存不够怎么办?

几个省显存的技巧(按优先级):

  1. gradient_checkpointing=True(已启用)
  2. 减小 per_device_train_batch_size,比如改成 1 或 2
  3. 增大 gradient_accumulation_steps 来补偿
  4. 减小 MAX_LENGTH,比如改成 256
  5. 使用 load_in_4bit=True(4-bit 量化,需要 bitsandbytes 库)

Q4:微调后的模型怎么部署?

LoRA 的增量权重可以和基础模型合并成一个完整模型:

merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged_model")
tokenizer.save_pretrained("./merged_model")

合并后就可以像普通模型一样加载使用了。

Q5:怎么判断微调有没有效果?

最简单的办法:微调前后用同一个问题测试,对比回答质量。

也可以看训练日志中的 loss(损失值):

  • Loss 持续下降 → 模型在学习
  • Loss 不降了甚至上升 → 可能过拟合了
  • 正常训练:Loss 从 2-3 降到 1 以下

Q6:数据格式不对怎么办?

本文用的是 JSON 格式(每行一条),你也可以用 CSV、Excel 等格式,只要数据预处理时正确解析即可。关键是最终要转换成 {"question": ..., "answer": ...} 的结构。


总结

通过本文,你学会了:

  1. 从 ModelScope 下载 Qwen3.5-4B 大模型
  2. 理解训练数据的格式和预处理方式
  3. 配置 LoRA 参数并启动训练
  4. 加载微调后的模型进行推理
步骤 文件 耗时 难度
下载模型 model_download.py 5-30 分钟
数据预处理 p2.py < 1 分钟 ⭐⭐
LoRA 训练 p3.py 10-30 分钟 ⭐⭐⭐
推理测试 p4.py < 1 分钟 ⭐⭐

核心思想:大模型微调 = 基础模型 + 专业数据 + LoRA 增量训练。就像给一个聪明的大学毕业生做岗前培训,让他成为特定领域的专家!


本文所用模型:Qwen3.5-4B
LoRA 技术参考:HuggingFace PEFT

更多推荐