7-大语言模型—指令理解:指令微调训练+模型微调

发布于:2025-07-21 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

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、三种方法对比与结合

方法 成本 质量 规模 领域适配性
手动构建
数据集转换 依赖原始数据
自动构建 中 - 低 极大 需验证

推荐组合策略

  1. 冷启动阶段:手动构建小规模高质量种子数据(如 1000 条)。
  2. 扩展阶段
    • 使用种子数据训练初始模型,通过自动构建生成大量候选数据。
    • 将现有公开数据集转换为指令格式,补充多样性。
  3. 优化阶段
    • 人工筛选自动生成的高质量样本,加入训练集。
    • 针对薄弱领域(如低资源语言、专业领域)补充手动构建数据。

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、实验结果

 


网站公告

今日签到

点亮在社区的每一天
去签到