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

📅 2026/7/19 22:51:36 👁️ 阅读次数 📝 编程学习
从零开始:Qwen3.5-4B 大模型 LoRA 微调实战教程

从零开始: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 显存8GB16GB+
内存16GB32GB
硬盘20GB50GB

2. 环境准备

安装必要的 Python 库

pipinstalltorch transformers datasets peft modelscope swanlab

各库的作用:

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

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

对应文件:model_download.py

frommodelscopeimportsnapshot_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

importjsonfromdatasetsimportDatasetfromtransformersimportAutoTokenizer# 1. 加载分词器tokenizer=AutoTokenizer.from_pretrained('./模型微调/Qwen/Qwen3___5-4B')# 2. 读取数据data=[]withopen("./data/llm_resume_scoring.json","r",encoding="UTF-8")asf:forlineinf:line=line.strip()ifline:data.append(json.loads(line))# 3. 转为 Dataset 格式ds=Dataset.from_list(data)

什么是 Tokenization?

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

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

处理函数详解

defprocess_func(example):MAX_LENGTH=512SYS="给定一个候选人的工作经历信息,你需要针对每个职位进行综合评分..."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# 截断到最大长度iflen(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(与上一步合并)

# 对所有数据进行 tokenizationtokenized_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 加载基础模型

importtorchfromtransformersimportAutoModelForCausalLM,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

frompeftimportLoraConfig,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 参数详解

参数含义建议值
rLoRA 的秩,越大能力越强,但也越慢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 配置训练参数

fromtransformersimportTrainingArguments 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.0001LoRA 常用值,收敛平稳
save_steps=100每 100 步存一次可以随时恢复训练

6.4 开始训练

fromtransformersimportTrainer,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

fromtransformersimportAutoModelForCausalLM,AutoTokenizerfrompeftimportPeftModelimporttorch# 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}withtorch.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.py5-30 分钟
数据预处理p2.py< 1 分钟⭐⭐
LoRA 训练p3.py10-30 分钟⭐⭐⭐
推理测试p4.py< 1 分钟⭐⭐

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


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