目录
4.3.1、 LoRA(Low-Rank Adaptation):给模型加 “临时支路”
4.3.2、 AdaLoRA(Adaptive LoRA):给 “重要支路” 加宽
4.3.3、QLoRA(Quantized LoRA):给支路加 “压缩包”
指令微调,是指在预训练大预言模型的基础上,通过使用有标注的自然语言形式的数据,对模型参数进行微调,使模型具备指令遵循能力,能够完成各类预先设计的任务,并可以在零样本情况下处理诸多下游任务。
1、指令微调的训练过程
分为三个步骤:
(1) 针对每一项任务明确地定义相应的自然语言形式的指令或者指示,这些指令或提示对任务目标及输出要求进行清晰描述。
(2)将训练数据调整成包含指令及与之对应的响应的形式
(3)使用包含指令和响应的训练数据对预训练模型进行微调操作。
2、指令微调数据
指令微调数据通常由文本构成,包含“指令输入”与“答案输出”两个关键部分。
2.1、“指令输入”
是指人们向模型提出的各类请求,包含定义精准、清晰的指令或者提示信息,其核心作用在于详细阐释任务的目标究竟是什么,以及明确规定输出需要满足的各项要求。指令涵盖的范畴极为广泛,包括问题回答、信息分类、内容总结、文本改写等。
2.2、“答案输出”
则是指期望模型依据所接收的指令响应内容,这些响应需要符合人们预先设定的期望。答案输出的内容,可以使用人工手段或借助自动化方法构建。
3、指令微调数据的构建方法
指令微调数据的构建方法主要分为手动构建、现有数据集转换和自动构建三种,以下是对这三种方法的详细解析:
3.1、手动构建:纯人工 “出题 + 写答案”
通过人工设计指令和标注响应,适合高质量、特定领域数据。
3.1.1、构建流程
3.1.1.1、定义任务类型
- 明确覆盖的任务类别(如问答、摘要、翻译、推理等)。
- 示例任务清单:
- 开放式问答(如解释科学概念) - 封闭式问答(如选择题) - 文本摘要(如新闻摘要) - 指令生成(如写邮件、代码) - 推理任务(如数学问题、逻辑推理)
3.1.1.2、设计指令模板
- 为每个任务类型创建多样化的指令模板,避免单一表述。
- 示例模板:
- 解释类:"请解释{概念}的原理" - 摘要类:"用{X}个字总结以下文本" - 翻译类:"将{源语言}翻译成{目标语言}:{文本}" - 改写类:"用{风格}改写以下句子:{文本}"
3.1.1.3、人工标注响应
- 标注者根据指令生成高质量响应,确保:
- 准确性:事实正确,无错误信息。
- 完整性:回答完整,不遗漏关键细节。
- 一致性:格式和风格统一(如使用项目符号、分段等)。
3.1.2、工具支持
- 标注平台:LabelStudio、Prodigy、Amazon SageMaker Ground Truth。
- 协作管理:使用 Notion、Google Sheets 管理任务分配和进度。
3.1.3、 优缺点
- 优点:数据质量高,符合特定领域需求,可控性强。
- 缺点:成本高(人力 + 时间),规模受限,难以覆盖长尾场景。
3.2、现有数据集转换:给 “旧材料” 换个 “新包装”
将公开数据集或私有数据转换为指令 - 响应格式,适合快速构建大规模数据。
很多领域早就有现成的数据集了(比如以前做翻译的双语数据、做分类的文本 + 标签数据),把这些 “旧数据” 改写成 “指令 + 答案” 的形式。
3.2.1、常见转换来源
3.2.1.1、问答数据集
- 示例:SQuAD、Natural Questions → 转换为指令格式。
- 转换方法:
# 原始数据:{"context": "巴黎是法国首都", "question": "法国首都在哪里", "answer": "巴黎"} # 转换后: {"instruction": "回答以下问题:法国首都在哪里", "output": "巴黎"}
3.2.1.2、摘要数据集
- 示例:CNN/Daily Mail、XSum → 转换为 “总结文本” 指令。
- 转换方法:
# 原始数据:{"article": "……", "summary": "……"} # 转换后: {"instruction": "总结以下新闻:{article}", "output": "{summary}"}
3.2.1.3、多轮对话数据集
- 示例:CoQA、DialogSum → 转换为多轮指令交互。
- 转换方法:
# 原始对话:[{"user": "今天天气如何?", "assistant": "晴天,25℃"}, {"user": "适合穿什么?", "assistant": "穿短袖和薄外套"}] # 转换后: {"instruction": "根据对话回答问题:今天适合穿什么?对话历史:今天天气如何?晴天,25℃", "output": "穿短袖和薄外套"}
3.2.1.4、专业领域数据
- 示例:医疗病历、法律文书 → 转换为领域特定指令。
- 转换方法:
# 原始病历:{"symptoms": "头痛、发热", "diagnosis": "流感"} # 转换后: {"instruction": "根据症状诊断疾病:头痛、发热", "output": "流感"}
3.2.2、 转换技巧
- 增加多样性:对同一原始数据,使用多个指令模板生成不同版本。
示例:# 原始数据:{"text": "苹果公司成立于1976年"} # 转换版本1: {"instruction": "提取关键信息:苹果公司成立于1976年", "output": "苹果公司,成立时间:1976年"} # 转换版本2: {"instruction": "将以下句子转换为问答形式:苹果公司成立于1976年", "output": "问题:苹果公司何时成立?答案:1976年"}
3.2.3、优缺点
- 优点:快速获取大规模数据,成本低,保留原始数据的领域知识。
- 缺点:格式适配可能复杂,需处理数据噪声,领域可能受限。
3.3、自动构建:让机器自己 “编题 + 答题”
利用模型或算法自动生成指令 - 响应数据,适合低成本扩展数据规模。
用已经训练好的模型(比如大模型本身)自动生成指令和答案,相当于让 “学生” 自己出题自己做。
3.3.1、基于大模型生成
3.3.1.1、自我指导(Self-Instruct)
- 流程:
1. 使用少量人工标注数据训练初始模型。 2. 用初始模型生成新的指令-响应样本。 3. 人工筛选高质量样本,扩充训练集。 4. 重复步骤2-3迭代优化。
- 工具:Hugging Face 的
self-instruct
库。
3.3.1.2、指令模板填充
- 方法:使用预训练模型填充指令模板中的变量。
- 示例:
运行
# 模板:"解释{概念}的{方面}在{领域}中的应用" # 填充后: {"instruction": "解释注意力机制的原理在自然语言处理中的应用", "output": "注意力机制……"}
3.3.2、基于规则生成
3.3.2.1、指令变体生成
- 方法:对现有指令进行语法改写、同义词替换等。
- 示例:
# 原始指令:"将这段文本翻译成英文" # 变体指令: ["请把这段文字转为英文", "翻译以下内容到英文", "用英文表达这段文本"]
3.3.2.2、响应合成
- 方法:从知识库或 API 获取信息,自动生成响应。
- 示例:
# 指令:"查询特斯拉公司2023年Q1营收" # 响应:通过调用财务API获取数据后生成。
3.3.3、混合方法
- 流程:
1. 使用规则生成基础指令-响应模板。 2. 用大模型对模板进行多样化扩展。 3. 通过人工或自动化筛选机制过滤低质量样本。
3.3.4、优缺点
- 优点:低成本、高效率,可大规模扩展数据。
- 缺点:生成质量可能参差不齐,需严格筛选机制,可能引入模型偏见。
3.4、三种方法对比与结合
方法 | 成本 | 质量 | 规模 | 领域适配性 |
---|---|---|---|---|
手动构建 | 高 | 高 | 小 | 强 |
数据集转换 | 中 | 中 | 大 | 依赖原始数据 |
自动构建 | 低 | 中 - 低 | 极大 | 需验证 |
推荐组合策略:
- 冷启动阶段:手动构建小规模高质量种子数据(如 1000 条)。
- 扩展阶段:
- 使用种子数据训练初始模型,通过自动构建生成大量候选数据。
- 将现有公开数据集转换为指令格式,补充多样性。
- 优化阶段:
- 人工筛选自动生成的高质量样本,加入训练集。
- 针对薄弱领域(如低资源语言、专业领域)补充手动构建数据。
4、模型微调
模型微调是让预训练大模型(比如 GPT、LLaMA 等)“专项进修” 的过程 —— 就像一个学了基础知识的大学生,通过针对性训练成为某领域专家(比如从 “全科生” 变成 “法律顾问” 或 “代码助手”)。
4.1、先搞懂:为什么需要模型微调?
预训练大模型(比如 GPT-3.5)已经通过海量数据学会了语言规律、常识等 “通用能力”,但直接用它做具体任务(比如公司内部的客服问答、特定行业的数据分析)往往不够精准。
比如:用通用大模型回答 “我们公司的退款政策是什么”,它可能瞎编;但如果用公司历史退款记录微调后,就能准确回答。
传统微调的问题:大模型参数太多(动辄几十亿、上千亿),直接训练所有参数就像 “重新教一遍”,又慢又费钱(需要顶级 GPU),还容易 “学歪”(忘记原有知识)。
4.2、参数高效微调:给大模型 “轻量补课”
为了解决传统微调的痛点,研究者想出了参数高效微调(PEFT) 方法:不碰原模型的大部分参数,只训练少量 “新增参数”,效果却能接近全量微调。
LoRA、AdaLoRA、QLoRA 都是 PEFT 的代表,核心思路类似 “给原模型加小插件,只训练插件”。
4.3、逐个拆解:LoRA、AdaLoRA、QLoRA
4.3.1、 LoRA(Low-Rank Adaptation):给模型加 “临时支路”
核心思想:不改动原模型的 “主干道”(大参数矩阵),而是新增两条 “临时支路”(小矩阵),让模型在训练时主要走支路,推理时再把支路合并回主干道。
打个比方:原模型的参数像一条宽马路,LoRA 在旁边修了两条窄巷子(A 和 B)。训练时,数据主要从巷子走(只优化 A 和 B);训练完,把巷子的 “流量” 合并到主马路,不影响原马路结构。
具体做法:
大模型里有很多 “注意力矩阵”(负责计算文字间的关联,比如 “猫” 和 “抓” 更相关),这些矩阵很大(比如 1024x1024)。
LoRA 把这些大矩阵拆成两个小矩阵(比如 1024x8 和 8x1024,“8” 是秩,可调整),只训练这两个小矩阵(参数从百万级降到万级),原矩阵冻结不动。优点:
- 训练速度快(参数少)、省显存(不用存原模型的梯度);
- 训练完合并参数后,推理速度和原模型一样(不增加额外计算)。
适用场景:大部分微调任务(比如文本分类、对话机器人),尤其是资源中等的情况(有一块较好的 GPU)。
4.3.2、 AdaLoRA(Adaptive LoRA):给 “重要支路” 加宽
核心思想:LoRA 的 “支路宽度”(秩)是固定的,但模型不同层的重要性不一样(比如有的层负责理解语义,有的层作用不大)。AdaLoRA 让 “重要的层” 支路宽一点(秩大),“不重要的层” 支路窄一点(秩小),更省资源。
打个比方:LoRA 给所有路段都修了同样宽的巷子,AdaLoRA 则根据路段的车流量(重要性)调整巷子宽度 —— 市中心(重要层)巷子宽(秩 = 16),郊区(次要层)巷子窄(秩 = 4)。
具体做法:
训练时动态计算每个层的 “贡献度”(比如该层对任务的影响多大),贡献高的层分配更大的秩(小矩阵更大),贡献低的层缩小秩甚至关掉支路。优点:比 LoRA 更高效,同样效果下参数更少(省 10%-30% 资源)。
适用场景:资源紧张但任务复杂的情况(比如多轮对话、长文本理解),需要精打细算用资源。
4.3.3、QLoRA(Quantized LoRA):给支路加 “压缩包”
核心思想:在 LoRA 基础上,给原模型参数 “瘦身”(量化),再训练支路。比如把原模型的参数从 “32 位浮点数” 压成 “4 位整数”(类似把高清图转成压缩图),大幅节省显存。
打个比方:原模型是一个 100GB 的大文件,QLoRA 先把它压缩成 10GB(但信息基本保留),再在压缩后的文件上修 LoRA 支路,训练时电脑只需要装下 10GB 文件 + 支路,普通电脑也能跑。
具体做法:
用 “4 位量化” 存储原模型参数(显存占用降为原来的 1/8),同时用 LoRA 训练新增的小矩阵。训练时通过 “量化感知训练” 保证精度不下降,最后合并参数。优点:资源要求极低,比如用消费级显卡(如 RTX 3090)就能微调 70 亿甚至 130 亿参数的大模型(传统方法需要几十块顶级 GPU)。
适用场景:个人或小团队微调大模型(比如用 LLaMA-7B 做私人助手),显存有限的情况(只有一块普通 GPU)。
4.4、三者对比:怎么选?
方法 | 核心改进 | 显存需求 | 适用场景 | 一句话总结 |
---|---|---|---|---|
LoRA | 固定秩的低秩分解 | 中 | 中等资源,通用任务 | 基础款 “轻量微调”,平衡速度和效果 |
AdaLoRA | 动态调整秩(按需分配) | 中低 | 资源紧张,复杂任务 | 智能款 “按需分配”,更省参数 |
QLoRA | 4 位量化 + LoRA | 极低 | 个人 / 小团队,大模型微调 | 平民款 “压缩微调”,普通电脑能跑 |
4.5、通俗总结
- 传统微调:给大模型 “全身体检 + 重训”,贵且麻烦;
- LoRA:只给大模型 “局部小手术”,快又省;
- AdaLoRA:“智能小手术”,哪里重要修哪里;
- QLoRA:“压缩后小手术”,普通设备也能做。
5、完整代码
# 导入必要的库
import json # 用于数据的序列化和反序列化
import random # 用于数据打乱,保证训练随机性
import torch # PyTorch核心库,用于张量计算和模型训练
import os # 用于文件路径操作和验证
import warnings # 用于屏蔽无关警告,保持输出整洁
from tqdm import tqdm # 用于显示进度条,直观展示训练/评估进度
from datasets import Dataset # Hugging Face的数据集类,用于数据格式转换
from transformers import (
AutoTokenizer, # 自动加载预训练模型的分词器
AutoModelForCausalLM, # 自动加载因果语言模型(如GPT-2)
TrainingArguments, # 训练参数配置类
Trainer, # 训练器类,封装了训练逻辑
BitsAndBytesConfig # 量化配置(当前代码禁用,保留为扩展接口)
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training # PEFT库,用于参数高效微调(LoRA)
from nltk.translate.bleu_score import sentence_bleu # 用于计算BLEU分数,评估生成文本质量
# 屏蔽无关警告(可选,根据需要开启)
# 屏蔽bitsandbytes库的警告(当前禁用量化,可注释)
warnings.filterwarnings("ignore", category=UserWarning, module="bitsandbytes")
# 屏蔽torch.checkpoint的reentrant参数警告
warnings.filterwarnings("ignore", category=UserWarning, message="torch.utils.checkpoint: the use_reentrant parameter")
# 设置随机种子,确保实验可复现
random.seed(42) # Python原生随机数种子
torch.manual_seed(42) # PyTorch随机数种子
# ----------------------
# 一、数据构建模块:生成三种类型的指令微调数据
# (解释类、写作类、分析类,覆盖典型微调场景)
# ----------------------
def manual_construction():
"""手动构建三类指令数据,作为基础样本"""
manual_data = [
# 1. 解释类:解释概念/技术
{
"instruction": "解释什么是机器学习",
"input": "", # 无额外输入
"output": "机器学习是人工智能的一个分支,它通过算法让计算机从数据中学习模式,而不需要明确的编程指令。常见的机器学习任务包括分类、回归、聚类等。"
},
# 2. 写作类:生成特定格式文本(邮件)
{
"instruction": "写一封申请实习的邮件",
"input": "申请岗位:AI研究员,公司:ABC科技,附学历证明", # 提供关键信息
"output": "尊敬的招聘经理:\n\n您好!我是XX大学计算机专业的本科生,预计2025年毕业。我对贵公司的AI研究方向非常感兴趣,特此申请AI研究员实习岗位。\n\n随信附上我的简历和学历证明,期待您的回复。\n\n此致\n敬礼\n申请人:张三"
},
# 3. 翻译类(属于写作类变体):语言转换
{
"instruction": "将以下文本翻译成英文",
"input": "深度学习已经彻底改变了自然语言处理领域。", # 待翻译文本
"output": "Deep learning has revolutionized the field of natural language processing."
},
# 4. 分析类:情感倾向分析
{
"instruction": "分析下面这段文字的情感倾向",
"input": "这款新手机的电池续航太差了,完全达不到宣传的效果!", # 待分析文本
"output": "负面。文本中使用了'太差了'这样的负面评价词,并且指出产品'完全达不到宣传的效果',表达了对手机电池续航的不满。"
},
# 5. 建议类(属于分析类变体):提供解决方案
{
"instruction": "给出三个提高代码可读性的建议",
"input": "", # 无额外输入
"output": "1. 使用有意义的变量名和函数名,避免使用无意义的缩写;2. 添加适当的注释,特别是复杂算法或关键逻辑部分;3. 遵循一致的代码风格和缩进规则,保持代码结构清晰。"
}
]
return manual_data
def dataset_conversion():
"""跳过外部数据集加载(如需扩展,可在此处添加其他数据集转换逻辑)"""
print("已跳过外部数据集加载")
return []
def auto_construction(num_samples=50):
"""自动生成50条样本,补充三类指令数据(保持数据多样性)"""
print("自动生成数据(本地模式,不加载外部模型)")
auto_data = []
# 用于生成"解释类"指令的概念列表
concepts = ["神经网络", "区块链", "量子计算", "自然语言处理", "大数据", "云计算"]
# 用于生成"写作类"指令的主题列表
themes = ["人工智能的发展趋势", "环境保护的重要性", "元宇宙的未来", "可再生能源的应用"]
# 用于生成"分析类"指令的文本列表
analyze_texts = [
"这家餐厅的服务特别好,菜品也很美味,下次还会再来",
"这个软件频繁崩溃,客服也不解决问题,非常失望",
"这部电影剧情紧凑,演员演技出色,强烈推荐"
]
for i in range(num_samples):
if i % 5 == 0: # 每5条样本生成1条"解释类"
concept = concepts[i % len(concepts)]
instruction = f"解释{concept}的工作原理"
output = f"{concept}是一种重要的技术,广泛应用于多个领域,通过特定的机制实现其功能。其核心原理包括数据输入、处理逻辑和结果输出三个环节。"
elif i % 5 == 1: # 每5条样本生成1条"写作类"
theme = themes[i % len(themes)]
instruction = f"写一篇关于{theme}的短文(100字左右)"
output = f"{theme}是当前社会关注的热点话题。随着技术进步和认知提升,其在经济、环境和社会层面的影响日益显著。深入研究其发展规律,对未来规划具有重要意义。"
elif i % 5 == 2: # 每5条样本生成1条"翻译类"(写作类变体)
instruction = "将以下句子改写成正式的表达方式"
input_text = "这个技术特别好用,大家都觉得不错"
output = "该技术具有较高的实用性,获得了广泛的认可与好评。"
elif i % 5 == 3: # 每5条样本生成1条"解释类"(补充)
instruction = "回答以下问题:什么是人工智能?"
output = "人工智能是研究如何使计算机模拟人类智能行为的科学与技术,涵盖机器学习、自然语言处理、计算机视觉等多个分支。"
else: # 剩余样本生成"分析类"
text = analyze_texts[i % len(analyze_texts)]
instruction = f"分析这段文字的情感倾向:{text}"
input_text = text
# 根据文本内容生成对应情感分析结果
if "好" in text or "美味" in text or "推荐" in text:
output = "正面。文本中使用了'好'、'美味'等积极词汇,表达了对事物的满意和推荐态度。"
else:
output = "负面。文本中使用了'崩溃'、'失望'等消极词汇,表达了对事物的不满情绪。"
auto_data.append({
"instruction": instruction,
"input": input_text if i % 5 == 2 or i % 5 == 4 else "", # 仅特定样本需要input
"output": output
})
return auto_data
def build_full_dataset():
"""组合手动和自动生成的数据,构建完整数据集"""
print("开始构建数据集...")
manual_data = manual_construction()
print(f"手动构建完成: {len(manual_data)} 个样本(含解释类、写作类、分析类)")
converted_data = dataset_conversion()
print(f"数据集转换完成: {len(converted_data)} 个样本")
auto_data = auto_construction()
print(f"自动构建完成: {len(auto_data)} 个样本(补充三类指令数据)")
# 合并数据并打乱顺序(避免同类样本集中)
full_data = manual_data + converted_data + auto_data
random.shuffle(full_data)
# 保存数据集到本地(方便后续查看和复用)
with open("instruction_tuning_data.json", "w", encoding="utf-8") as f:
json.dump(full_data, f, ensure_ascii=False, indent=2)
print(f"数据集构建完成,共{len(full_data)}个样本(含三类指令)")
return full_data
# ----------------------
# 二、模型微调模块:使用LoRA进行参数高效微调
# ----------------------
def finetune_model(dataset):
print("开始模型微调(本地模式)")
# 本地预训练模型路径(需提前下载GPT-2中文模型)
model_name = r"E:\WH\data\gpt2-chinese-cluecorpussmall"
# 验证模型路径是否存在(避免路径错误导致加载失败)
print(f"正在验证模型路径: {model_name}")
if not os.path.exists(model_name):
raise FileNotFoundError(f"模型路径不存在: {model_name}")
# 检查路径下是否包含必要的模型文件(确保模型完整)
required_files = ["config.json", "pytorch_model.bin", "tokenizer_config.json", "vocab.txt"]
missing_files = [f for f in required_files if not os.path.exists(os.path.join(model_name, f))]
if missing_files:
raise FileNotFoundError(f"模型路径缺少必要文件: {', '.join(missing_files)}")
print(f"模型路径验证通过: {model_name}")
print(f"正在加载本地模型...")
# 加载分词器(将文本转换为模型可识别的token)
try:
tokenizer = AutoTokenizer.from_pretrained(model_name)
# GPT-2默认无pad_token,需手动设置为eos_token(确保批量处理时填充有效)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
print(f"已将填充标记设置为:{tokenizer.pad_token}(与结束标记一致)")
except Exception as e:
raise Exception(f"加载分词器失败,请检查路径是否正确:{model_name}\n错误:{e}")
# 格式化数据集:将instruction/input/output转换为模型训练的prompt格式
def format_dataset(dataset):
formatted_data = []
for example in dataset:
instruction = example["instruction"]
input_text = example["input"]
output = example["output"]
# 区分有/无input的情况,保持prompt格式统一
if input_text:
prompt = f"### Instruction: {instruction}\n### Input: {input_text}\n### Response: {output}"
else:
prompt = f"### Instruction: {instruction}\n### Response: {output}"
formatted_data.append({"text": prompt}) # 用"text"字段统一存储
return formatted_data
# 转换为Hugging Face Dataset格式(方便后续分词处理)
formatted_data = format_dataset(dataset)
hf_dataset = Dataset.from_list(formatted_data)
# 分词函数:将文本转换为token ID,并添加labels用于计算损失
def tokenize_function(examples):
# 分词时自动填充/截断到固定长度(512,根据模型最大序列长度设置)
tokenized = tokenizer(
examples["text"],
padding="max_length", # 不足512则填充
truncation=True, # 超过512则截断
max_length=512
)
# GPT-2是自回归模型,labels与input_ids一致(用输入预测输出)
tokenized["labels"] = tokenized["input_ids"].copy()
return tokenized
# 批量分词处理(加速处理效率)
tokenized_dataset = hf_dataset.map(tokenize_function, batched=True)
print(f"数据集分词完成,共{len(tokenized_dataset)}个样本(每个样本已转换为512长度的token)")
# 配置LoRA(参数高效微调方法,只训练部分参数,减少资源消耗)
lora_config = LoraConfig(
r=8, # LoRA注意力维度(控制参数量,越大能力越强但训练越慢)
lora_alpha=32, # 缩放因子(通常为r的4倍)
target_modules=["c_attn", "c_proj"], # GPT-2中需要微调的注意力模块
lora_dropout=0.1, # Dropout概率(防止过拟合)
bias="none", # 不微调偏置参数
task_type="CAUSAL_LM" # 任务类型:因果语言模型
)
# 加载预训练模型(不使用量化,适合CPU/GPU环境)
try:
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto" # 自动分配设备(CPU/GPU)
)
except Exception as e:
raise Exception(f"加载模型失败,请检查路径是否正确:{model_name}\n错误:{e}")
# 准备模型训练(关闭缓存,适配LoRA)
model.config.use_cache = False # 关闭缓存,避免与梯度检查点冲突
# 根据PEFT版本选择是否添加use_reentrant参数(兼容新旧版本)
import peft
if hasattr(peft, '__version__') and peft.__version__ >= "0.7.0":
model = prepare_model_for_kbit_training(model, use_reentrant=False)
else:
model = prepare_model_for_kbit_training(model)
# 应用LoRA配置(将LoRA适配器注入模型)
model = get_peft_model(model, lora_config)
# 打印可训练参数比例(验证LoRA是否生效)
print(f"LoRA配置完成,可训练参数占比: {model.print_trainable_parameters()}")
# 配置训练参数(根据硬件调整,CPU训练需减小批次)
training_args = TrainingArguments(
output_dir="./results", # 训练结果保存路径
learning_rate=3e-4, # 学习率(LoRA通常用较大学习率)
per_device_train_batch_size=2, # 单设备批次大小(CPU设为2,GPU可增大)
gradient_accumulation_steps=8, # 梯度累积步数(等效批次=2*8=16)
num_train_epochs=3, # 训练轮数(3轮足够小数据集)
weight_decay=0.01, # 权重衰减(防止过拟合)
logging_dir="./logs", # 日志保存路径
logging_steps=1, # 每1步打印一次损失
save_strategy="epoch", # 每轮结束保存模型
fp16=False, # CPU模式关闭混合精度训练
dataloader_pin_memory=False, # CPU关闭内存锁定
disable_tqdm=False, # 启用进度条
report_to="none" # 不使用外部日志工具(如W&B)
)
# 创建训练器(封装训练逻辑)
trainer = Trainer(
model=model, # 待训练的模型
args=training_args, # 训练参数
train_dataset=tokenized_dataset # 训练数据集
)
# 开始训练
print(
f"开始训练(共{training_args.num_train_epochs}轮,每轮{len(tokenized_dataset) // training_args.per_device_train_batch_size}步)")
train_result = trainer.train()
# 训练结束提示(展示最终损失,判断是否收敛)
print("\n" + "=" * 50)
print(f"训练已全部完成!共训练{training_args.num_train_epochs}轮")
print(f"最终训练损失:{train_result.training_loss:.4f}(损失越低说明拟合越好)")
print("=" * 50 + "\n")
# 保存微调后的模型(仅保存LoRA适配器,体积小)
model_save_path = "instruction_tuned_model"
model.save_pretrained(model_save_path)
tokenizer.save_pretrained(model_save_path)
# 验证模型保存结果
print(f"验证微调模型保存路径: {model_save_path}")
if not os.path.exists(model_save_path):
raise FileNotFoundError(f"模型保存失败,路径不存在: {model_save_path}")
saved_files = os.listdir(model_save_path)
print(f"保存的模型文件: {', '.join(saved_files)}(应包含adapter_config.json和adapter_model.bin)")
print(f"模型微调完成,已保存至 {model_save_path}")
return model_save_path, tokenizer
# ----------------------
# 三、测试评估模块:分别测试三类指令微调效果
# ----------------------
def test_model(model_path, tokenizer):
"""测试三类指令的微调效果:解释类、写作类、分析类"""
print("开始模型测试...")
# 验证模型路径
if not os.path.exists(model_path):
raise FileNotFoundError(f"测试模型路径不存在: {model_path}")
# 加载微调后的模型
model = AutoModelForCausalLM.from_pretrained(
model_path,
device_map="auto" # 自动分配设备
)
# 生成回答的函数(封装生成逻辑,方便复用)
def generate_response(model, tokenizer, instruction, input_text="", max_length=200):
# 构造与训练时一致的prompt格式
prompt = f"### Instruction: {instruction}\n### Input: {input_text}\n### Response:"
# 转换为模型输入格式
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
# 生成回答(控制随机性和长度)
outputs = model.generate(
inputs.input_ids,
max_length=max_length, # 最大长度
temperature=0.7, # 温度(0.7表示中等随机性)
num_return_sequences=1 # 生成1个结果
)
# 解码并提取回答部分
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
return response.split("### Response:")[-1].strip() # 只保留回答内容
# 三类指令测试案例(覆盖微调场景)
test_instructions = [
# 1. 解释类(新概念,测试知识迁移能力)
{
"instruction": "解释什么是边缘计算",
"input": "",
"type": "解释类",
"expected": "应说明边缘计算的定义、特点(如靠近数据源、低延迟)和应用场景"
},
# 2. 写作类(新场景,测试格式生成能力)
{
"instruction": "写一封产品退款申请邮件",
"input": "产品名称:无线耳机,问题:无法充电,购买时间:2025年6月1日",
"type": "写作类",
"expected": "应包含礼貌称呼、退款原因、关键信息(产品/时间)和诉求,格式符合邮件规范"
},
# 3. 分析类(新文本,测试情感判断能力)
{
"instruction": "分析下面这段文字的情感倾向",
"input": "这款智能手表续航超预期,功能丰富,性价比很高!",
"type": "分析类",
"expected": "应判断为正面情感,并说明依据(如'超预期'、'性价比高'等积极词汇)"
}
]
# 执行测试并打印结果
for i, test_case in enumerate(test_instructions, 1):
instruction = test_case["instruction"]
input_text = test_case["input"]
case_type = test_case["type"]
expected = test_case["expected"]
response = generate_response(model, tokenizer, instruction, input_text)
print(f"\n=== 测试案例 {i}({case_type}): {instruction} ===")
if input_text:
print(f"输入信息: {input_text}")
print(f"模型回答: \n{response}")
print(f"预期表现: {expected}")
print("-" * 80)
return model, tokenizer
def evaluate_model(model, tokenizer, test_data, num_samples=20):
"""用BLEU分数自动评估模型生成质量(数值越高越好,0-1之间)"""
print(f"开始自动评估(使用前{num_samples}个样本,计算BLEU-1分数)...")
bleu_scores = []
# 生成回答的函数(与测试函数一致,保证评估逻辑统一)
def generate_response(model, tokenizer, instruction, input_text="", max_length=200):
prompt = f"### Instruction: {instruction}\n### Input: {input_text}\n### Response:"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(
inputs.input_ids,
max_length=max_length,
temperature=0.7,
num_return_sequences=1
)
return tokenizer.decode(outputs[0], skip_special_tokens=True).split("### Response:")[-1].strip()
# 遍历样本计算BLEU分数(用进度条显示)
for example in tqdm(test_data[:num_samples], desc="评估进度"):
instruction = example["instruction"]
input_text = example["input"]
reference = example["output"] # 参考回答(人工标注)
response = generate_response(model, tokenizer, instruction, input_text) # 模型回答
# 计算BLEU-1分数(单字匹配度,适合中文)
reference_tokens = list(reference) # 参考回答分词(按字)
response_tokens = list(response) # 模型回答分词(按字)
bleu_score = sentence_bleu([reference_tokens], response_tokens, weights=(1, 0, 0, 0)) # 只关注单字匹配
bleu_scores.append(bleu_score)
# 计算平均分数
avg_bleu = sum(bleu_scores) / len(bleu_scores) if bleu_scores else 0
print(f"平均BLEU-1分数: {avg_bleu:.4f}(>0.3表示微调有效,>0.5表示效果较好)")
return avg_bleu
# ----------------------
# 主程序:串联数据构建→微调→测试→评估全流程
# ----------------------
if __name__ == "__main__":
print("完全本地模式运行,无任何网络依赖")
# 1. 构建数据集(包含三类指令数据)
dataset = build_full_dataset()
# 2. 微调模型(用LoRA适配三类指令)
model_path, tokenizer = finetune_model(dataset)
# 3. 测试模型(分别验证解释类、写作类、分析类指令)
model, tokenizer = test_model(model_path, tokenizer)
# 4. 评估模型(用BLEU分数量化微调效果)
evaluate_model(model, tokenizer, dataset)
print("\n=== 指令微调流程全部完成 ===")
6、实验结果