7-大语言模型—指令理解:指令微调训练+模型微调
目录
1、指令微调的训练过程
2、指令微调数据
2.1、“指令输入”
2.2、“答案输出”
3、指令微调数据的构建方法
3.1、手动构建:纯人工 “出题 + 写答案”
3.1.1、构建流程
3.1.1.1、定义任务类型
3.1.1.2、设计指令模板
3.1.1.3、人工标注响应
3.1.2、工具支持
3.1.3、 优缺点
3.2、现有数据集转换:给 “旧材料” 换个 “新包装”
3.2.1、常见转换来源
3.2.1.1、问答数据集
3.2.1.2、摘要数据集
3.2.1.3、多轮对话数据集
3.2.1.4、专业领域数据
3.2.2、 转换技巧
3.2.3、优缺点
3.3、自动构建:让机器自己 “编题 + 答题”
3.3.1、基于大模型生成
3.3.1.1、自我指导(Self-Instruct)
3.3.1.2、指令模板填充
3.3.2、基于规则生成
3.3.2.1、指令变体生成
3.3.2.2、响应合成
3.3.3、混合方法
3.3.4、优缺点
3.4、三种方法对比与结合
4、模型微调
4.1、先搞懂:为什么需要模型微调?
4.2、参数高效微调:给大模型 “轻量补课”
4.3、逐个拆解:LoRA、AdaLoRA、QLoRA
4.3.1、 LoRA(Low-Rank Adaptation):给模型加 “临时支路”
4.3.2、 AdaLoRA(Adaptive LoRA):给 “重要支路” 加宽
4.3.3、QLoRA(Quantized LoRA):给支路加 “压缩包”
4.4、三者对比:怎么选?
4.5、通俗总结
5、完整代码
6、实验结果
指令微调,是指在预训练大预言模型的基础上,通过使用有标注的自然语言形式的数据,对模型参数进行微调,使模型具备指令遵循能力,能够完成各类预先设计的任务,并可以在零样本情况下处理诸多下游任务。
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_datadef 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_datadef 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_datarandom.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_tokenprint(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 peftif 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, tokenizerdef 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 0print(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、实验结果