🚀 大模型微调不迷路(下篇):推理、合并与“深度思考”解析
在上一篇中,我们成功让 Qwen3-8B 读完了“关键词抽取”的特训教材,并保存了一本名为 LoRA 的“外挂小账本”。
现在,我们要解析的是推理与测试代码(sft-LoRA-inference.ipynb)。这部分代码的任务是:把大模型和它的“小账本”结合起来,进行实战测试,并最终把小账本里的知识永久“刻”进大模型的脑子里(权重合并),方便以后独立部署。
话不多说,我们直接开拆!
💡 核心概念:什么是“合并与卸载”(Merge and Unload)?
如果说基础模型(Base Model)是一个基础扎实的大学生,LoRA 就是他专门为了这次考试做的一本“速记手册”。
- 带着手册考试(不合并):每次遇到问题,他都要一边回忆基础知识,一边翻手册。这样虽然能答题,但速度会变慢(存在额外的计算开销)。
- 把手册背下来(合并):他把手册里的知识完全融会贯通,变成了自己的直觉。之后去考场,他甚至不需要带手册了,答题速度飞快!这就是
merge_and_unload在做的事情。
🛠️ 核心代码全拆解
第一步:让“大学生”背下“速记手册”
model_name = "Qwen/Qwen3-8B"
# 1. 先把没看手册的“裸考”大学生叫过来
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
dtype=torch.float16
)
# 2. 把他写的“速记手册”(训练好的LoRA权重)递给他
peft_model = PeftModel.from_pretrained(
base_model, "/root/autodl-tmp/sft/Qwen3-8B/sft-LoRA/best", dtype=torch.float16
)
# 3. 让他把手册背下来,并把实体手册扔掉(合并权重并卸载LoRA层)
merged_model = peft_model.merge_and_unload()
经过这一步,你的 merged_model 已经是一个包含了微调知识的、原汁原味的 Qwen3 模型了。它的运行速度和原始模型一模一样。
第二步:模拟真实的对话场景
大模型不能直接吃纯文本,它需要一套严格的“对话礼仪”(Chat Template)。
messages = [
{"role": "user", "content": prompt}
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False
)
apply_chat_template:自动帮你把纯文本包装成大模型认识的格式(比如加上<|im_start|>和<|im_end|>这种标记)。add_generation_prompt=True:【关键参数】 它会在文本最后加上一个<|im_start|>assistant,就像是在面试时对模型说:“现在话筒交给你,请开始你的表演。”
第三步:生成回答与“解剖”思维链
这是整段代码里最秀的一部分操作!现在的先进模型(如 Qwen3)会先“打草稿”(输出 <think>...</think>),再给出最终答案。代码巧妙地把这两个部分剥离开来了。
# 让模型生成回答
generated_ids = merged_model.generate(
**model_inputs,
max_new_tokens=32768
)
# ... 前面省略了部分截取代码 ...
# 寻找 </think> 的位置
try:
index = len(output_ids) - output_ids[::-1].index(151668)
except ValueError:
index = 0
max_new_tokens=32768:允许模型最多说多少个词。给得非常大,防止它话说一半被强行闭麦。151668:【硬核数字】 这是 Qwen 分词器中</think>这个特殊符号对应的数字 ID。[::-1].index(151668):通过把列表倒过来找,精准定位到模型“打草稿”结束的位置。这样你就可以完美地把“思考过程”和“最终提取的关键词”分开打印,非常适合用来做 Agent 节点的输出解析!
💻 完整实战代码(带超详细注释)
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# ==========================================
# 1. 环境配置
# ==========================================
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HOME"] = "/root/autodl-tmp/hf"
# ==========================================
# 2. 模型加载与“灵魂融合”(合并权重)
# ==========================================
model_name = "Qwen/Qwen3-8B"
# 定义之前训练好的 LoRA 权重路径
lora_path = "/root/autodl-tmp/sft/Qwen3-8B/sft-LoRA/best"
# 定义合并后最终模型的保存路径
merged_save_path = "/root/autodl-tmp/sft/Qwen3-8B/sft-LoRA/merged"
print("🧠 正在加载分词器和基础模型...")
tokenizer = AutoTokenizer.from_pretrained(model_name)
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
dtype=torch.float16 # 推理时使用 FP16 即可
)
print("🔗 正在挂载 LoRA 权重...")
peft_model = PeftModel.from_pretrained(
base_model, lora_path, dtype=torch.float16
)
print("✨ 正在将 LoRA 知识永久刻入基础模型 (Merge and Unload)...")
# 【核心操作】将旁路矩阵与原权重做物理加法,从此告别额外的推理开销
merged_model = peft_model.merge_and_unload()
# ==========================================
# 3. 准备测试问题
# ==========================================
prompt = """抽取出文本中的关键词:
标题:人工神经网络在猕猴桃种类识别上的应用
文本:在猕猴桃介电特性研究的基础上,将人工神经网络技术应用于猕猴桃的种类识别.该种类识别属于模式识别,其关键在于提取样品的特征参数,在获得特征参数的基础上,选取合适的网络通过训练来进行识别.猕猴桃种类识别的研究为自动化识别果品的种类、品种和新鲜等级等提供了一种新方法,为进一步研究果品介电特性与其内在品质的关系提供了一定的理论与实践基础."""
messages = [
{"role": "user", "content": prompt}
]
# 套用对话模板
text = tokenizer.apply_chat_template(
messages,
tokenize=False, # 返回纯字符串,方便排错
add_generation_prompt=True, # 告诉模型:轮到你回答了
enable_thinking=False # 控制聊天模板中是否有默认的思考tag提示(不同模型行为可能有异)
)
# 把文字转成数字 ID,并送到 GPU 上(.to(merged_model.device))
model_inputs = tokenizer([text], return_tensors="pt").to(merged_model.device)
# ==========================================
# 4. 执行推理生成
# ==========================================
print("📝 模型正在思考中...")
generated_ids = merged_model.generate(
**model_inputs,
max_new_tokens=32768 # 限制最大生成长度
)
# 剥离掉用户输入的那部分 ID,只保留模型新生成的部分
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
# ==========================================
# 5. 精准解剖输出(分离思考过程与答案)
# ==========================================
try:
# 巧妙利用 Python 列表的倒序 [::-1] 查找最后一个 </think> 标签的 ID (151668)
# 找到它的位置后,就能把数组一切为二
index = len(output_ids) - output_ids[::-1].index(151668)
except ValueError:
# 如果没找到 </think>,说明模型直接给出了答案,没有打草稿
index = 0
# 解码 </think> 之前的部分(草稿区)
thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
# 解码 </think> 之后的部分(正式答案区)
content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n")
print("\n--- 🧠 模型的内部思考过程 ---")
print(thinking_content if thinking_content else "(无思考过程)")
print("\n--- ✅ 最终提取的关键词 ---")
print(content)
# ==========================================
# 6. 保存最终大一统模型
# ==========================================
print(f"\n💾 正在将完整模型保存至:{merged_save_path}")
# 保存合并后的模型,这个文件夹可以直接用 vLLM 或 Ollama 等工具进行高性能部署部署了!
merged_model.save_pretrained(merged_save_path)
tokenizer.save_pretrained(merged_save_path)
print("🎉 恭喜!模型微调与导出全流程通关!")
有了这份保存下来的 merged 文件夹,你相当于获得了一个完全属于你的、“出厂自带”关键词抽取技能的大模型。无论你是想用代码调用,还是部署成本地的 API 接口,都不再需要依赖当初的 LoRA 环境了!
:推理、合并与“深度思考”解析&spm=1001.2101.3001.5002&articleId=161582459&d=1&t=3&u=0ee101175b9b4fffa18335ec068ef22d)
1

被折叠的 条评论
为什么被折叠?



