跳到主要内容

课程说明:

  同学们好呀~欢迎来到《2025大模型原理与训练》试学体验课!我是课程主讲老师,九天。本期体验课将带领各位同学手动从零到一完成miniDeepSeek大模型训练,完整介绍Ubuntu系统使用、分词器训练、大模型预训练、大模型全量指令微调,以及DPO强化学习微调完整流程!

  • 体验课内容节选自《2025大模型原理与训练》完整版付费课程

  体验课时间有限,若想深度学习大模型技术,欢迎大家报名由我和菜菜老师主讲的《2025大模型原理与实战课程》

ae25c01c0cb13278a4b4cf636b940fa 1736422649885

《2025大模型原理与实战课程》 上新特惠进行中!限量福利30席售罄即止!课程大纲获取、领取体验课学员专享优惠券,扫码添加课程助教,回复“训练”,即咨询课程信息哦👇

c6847a817fd7efd0cddcb1bcac217c3 image-20250107200452887 image-20250107200502389

此外,miniDeepSeek-v3全部训练项目代码、数据、以及训练完的模型,都已上传至课件网盘,扫码联系助教即可领取。

image-20250109200311025

《2025大模型原理与训练》体验课

从零手动复现DeepSeek v3

Ch.3 MiniDeepSeek v3 后训练过程


【补充介绍】全量指令微调概念补充介绍

  • 从补全模型到对话模型

  全量指令微调(Full Instruction Tuning)在现代大规模预训练模型的应用中具有重要的意义。虽然预训练模型在大量的无监督数据上获得了强大的语言理解和生成能力,但这些模型通常缺乏处理具体任务的针对性表现。全量指令微调的核心作用在于将预训练模型进一步调整,使其能够根据明确的指令执行特定任务。

image-20241024161306985 image-20241024161934198
  • 全量指令微调的必要性
  1. 增强模型的任务适应能力

    • 预训练阶段的语言模型往往是通用的,并没有针对某个特定任务进行优化。虽然它们具备强大的生成和理解能力,但在实际应用中,我们常常需要模型执行某些具体的任务,例如对话生成、问答、文本摘要、翻译等。全量指令微调通过在明确的指令或任务定义下对模型进行进一步优化,提升了模型对这些任务的适应能力。
  2. 提升模型对多样化任务的执行能力

    • 指令微调的一个关键特征是它能够帮助模型学会根据不同的输入指令生成相应的输出。通过微调,模型能够处理广泛的任务类型,并根据指令灵活应对不同的任务需求。这个过程让模型从一个纯语言生成器转变为一个能够执行多种任务的通用人工智能系统。
  3. 缩小预训练模型与应用场景的差距

    • 预训练阶段的数据大多来自通用的文本数据,而模型实际应用中的任务和数据形式可能与预训练数据差异较大。通过全量指令微调,模型可以学习到如何在特定任务下调整其生成和决策方式,确保在实际应用中的表现更加精准和高效。这种微调过程弥补了预训练模型和实际应用场景之间的差距。
  4. 减少下游任务的数据需求

    • 全量指令微调使得模型在接受指令后,能够直接执行多个任务,而不再需要为每个任务单独进行微调或标注大量数据。通过在多任务、多领域的指令数据上进行微调,模型能够以较少的数据应对大量的任务,提升了其广泛的适用性。
  5. 提高用户交互的可控性

    • 通过指令微调,模型可以更好地理解和执行用户的明确需求,这使得模型的输出更加符合用户预期。用户可以通过简单明确的指令控制模型行为,而不再依赖复杂的上下文提示。这种可控性对于提供高质量的自动化服务和用户交互尤为重要。

  全量指令微调是使预训练模型更加实用和高效的关键步骤。它不仅提升了模型的任务执行能力,还使得模型更加灵活、可控和广泛适应多样化的应用场景。这使得指令微调成为在现代大规模语言模型开发和应用中不可或缺的一部分。

image-20250109181331251
  • 预训练模型和指令微调模型不同的适用场景

  不过需要注意的是,其实预训练模型指令微调模型各自都有不同的适用场景,尽管指令微调模型在特定任务上表现更好,但预训练模型也有其独特的优势和适用领域。接下来,我们可以比较它们各自适用的场景,并解释为什么在某些情况下预训练模型仍然有它的价值。

预训练模型的适用场景
  1. 通用语言理解任务

    • 场景:预训练模型经过大量无监督文本数据的训练,具备广泛的语言理解能力,能够在没有明确任务定义的情况下生成文本或做出推理。
    • 例子:生成文章、写作辅助、文本补全等需要较强语言理解和生成能力的任务。
    • 原因:预训练模型没有被限制在特定任务上,因此它可以很好地处理广泛的语言生成需求,并且在一些开放领域的应用中表现出色。
  2. 低资源或无监督学习任务

    • 场景:在数据有限或者缺乏标注数据的场景下,预训练模型可以直接应用于一些无监督或少量数据的任务,如文本分类、情感分析等。
    • 例子:当你没有足够的任务数据进行微调时,预训练模型可以通过自监督学习生成文本或进行特征提取。
    • 原因:预训练模型的核心优势在于它从大量无标注数据中学到的广泛的语言模式,这让它即使在没有具体任务数据的情况下也能展现出良好的表现。
  3. 跨领域或未知任务的探索

    • 场景:当你需要在未知领域或者全新任务上探索模型的表现时,预训练模型可以作为一个强大的基础模型进行初步的探索和实验。
    • 例子:如果你需要构建一个多领域的生成任务,预训练模型可以提供灵活性,允许你在不同任务之间切换,而不需要进行专门的任务微调。
    • 原因:由于预训练模型没有被微调锁定在某一个任务上,它可以应用于不同领域、不同形式的输入,而不受任务特定的限制。
  4. 探索性的文本生成

    • 场景:当任务的目标是生成富有创造力、探索性或多样化的文本时,预训练模型由于其没有特定任务的约束,可能会生成更具多样性和创造力的文本。
    • 例子:文学写作、诗歌创作、广告文案等需要高自由度生成的任务。
    • 原因:预训练模型的多样化语言生成能力让它能够在这些没有明确限制的场景中表现得更加灵活。
指令微调模型的适用场景
  1. 明确任务驱动的场景

    • 场景:在指令微调模型中,模型经过了明确的任务定义和微调,能够很好地理解并执行用户给定的指令。
    • 例子:问答系统、文本摘要、机器翻译等有明确指令的任务。
    • 原因:指令微调模型能够根据特定任务中的指令生成高质量、符合任务需求的输出。它通过微调,在特定任务上表现更加精确。
  2. 多任务统一处理

    • 场景:在需要统一处理多个任务的情况下,指令微调模型非常适合。这些模型能够根据指令动态适应不同的任务需求,而无需为每个任务单独训练不同的模型。
    • 例子:集成多种能力的智能助手,能够根据不同的指令生成对话、翻译文本、生成摘要等多种任务的结果。
    • 原因:指令微调模型通过在多任务数据上的训练,具备根据不同指令处理不同任务的能力,这使得它可以灵活应对复杂的多任务场景。
  3. 高精度特定任务

    • 场景:当任务需要高精度的结果,例如法律文档分析、医疗问答等具有高专业性要求的任务时,指令微调模型可以通过微调后的知识提升在特定任务上的表现。
    • 例子:法律问题解答、医疗诊断建议生成等任务。
    • 原因:指令微调模型可以在这些任务特定的领域数据上进行进一步的优化,从而在输出结果的准确性和专业性上更有保障。
  4. 高效用户交互

    • 场景:在需要与用户进行高效交互的系统中,指令微调模型能够准确理解用户的意图,并根据具体指令生成用户期望的答案或结果。
    • 例子:智能客服系统、虚拟助手、对话生成等任务。
    • 原因:指令微调模型通过学习用户指令的意图,能够更好地理解并生成符合用户预期的结果,提升交互体验。
预训练模型 vs. 指令微调模型适用场景的比较
特点预训练模型指令微调模型
任务灵活性适用于通用任务,能在没有特定任务数据的情况下进行探索性生成和任务处理针对明确任务进行优化,能根据特定指令执行高效、精确的任务
任务精确性生成的内容更加通用,适用于开放领域的任务生成内容更加准确,特别适合需要精确输出的应用场景
多任务处理能力可处理广泛的任务,但没有经过微调,在特定任务上的表现可能不如微调模型能根据指令灵活处理多个任务,在多任务环境中表现优秀
资源要求通常预训练模型在推理时资源需求较大,但在未微调的场景下灵活度较高微调后模型在指定任务上表现出色,但可能在资源上有较高要求,尤其是多任务微调的情况下
无监督学习应用在没有标注数据的场景下,预训练模型可以直接用于无监督任务通常需要在标注数据上进行微调,因此在标注数据不足的情况下,微调模型不如预训练模型适用
用户交互可以根据通用的用户输入生成合理的文本,但缺乏对特定指令的理解能更好地根据用户的指令执行任务,提升用户交互体验
探索性和创造性更加适合开放性任务,能生成探索性、创造性较强的文本在任务明确时表现优异,但在过于开放的任务中,生成结果的多样性和创造力可能不如预训练模型
总结:
  • 预训练模型:适用于开放领域任务、通用生成、低资源场景、跨领域探索和无监督学习任务。它在没有明确任务要求时非常灵活,可以直接应用于多个任务场景。

  • 指令微调模型:适合有明确指令需求的任务、高精度的特定任务、多任务处理以及需要高效用户交互的应用。它在根据指令进行高效任务执行和准确性要求较高的任务中表现出色。

  • 全量指令微调的一般流程

  1. 数据准备

    • 任务定义与指令设计
      • 在进行全量指令微调之前,必须明确微调模型需要执行的具体任务。这些任务可以是对话生成、问答系统、文本分类、机器翻译等。在数据准备阶段,需要为每个任务设计一组明确的指令,以指导模型的行为。例如,输入文本可以是 “给定一段话,请生成摘要”,而期望的输出是该段话的简洁概述。
    • 多样化的指令数据集
      • 指令微调的数据集通常包括多任务、多领域的数据,以增强模型的广泛适应能力。这些数据集可以来自多个来源,比如自然语言处理(NLP)任务数据集或用户交互记录。数据集的多样性越高,模型在面对不同任务时的表现通常越好。
    • 指令格式化
      • 在数据集中,输入的文本通常被格式化为“指令 + 输入”的形式,模型需要根据这组输入生成相应的输出。为了保持一致性,所有任务的数据都应遵循统一的格式标准,例如:
        指令: 给定以下问题,请生成合理的回答。
        输入: 问题: 世界上最高的山是什么?
        输出: 世界上最高的山是珠穆朗玛峰。
  2. 模型配置

    • 预训练模型加载
      • 微调的基础是已经预训练好的大模型。这些模型通常具有通用的语言理解能力。加载预训练模型时,需确保模型具备良好的初始化权重,使其能够较快适应指令微调任务。
    • 调整模型参数
      • 在模型配置阶段,你可能需要根据指令微调任务的复杂性调整模型参数。比如,增加 max_seq_len 以处理较长的指令输入,或调整 vocab_size 以处理新的领域词汇。确保模型结构足够灵活,能够适应不同的任务需求。
  3. 指令微调

    • 多任务微调
      • 全量指令微调的一个特点是它通常在多任务数据上同时进行训练。这意味着模型将同时接触来自不同任务的数据,并根据不同的指令生成相应的输出。训练时,模型会学习如何根据不同类型的输入指令作出正确的响应。
    • 损失函数设计
      • 在微调阶段,损失函数的选择尤为重要。通常使用交叉熵损失函数来衡量模型输出与预期结果之间的差距。此外,如果任务之间的目标差异较大,可以对不同任务设置权重,从而引导模型重点关注某些特定的任务。
    • 学习率和优化器
      • 选择合适的学习率和优化器对模型的微调至关重要。一般来说,可以使用较小的学习率(例如 1e-5 或 5e-5)来避免对预训练模型权重进行过度修改。AdamW 是较为常见的优化器,因为它在大规模语言模型微调任务中表现稳定。
  4. 训练与监控

    • 训练过程
      • 在训练过程中,模型会根据指令和输入生成对应的输出,优化器会根据损失函数不断更新模型的权重。在这个过程中,确保模型训练稳定并且逐渐提高在各个任务上的表现。
    • 使用监控工具
      • 在训练过程中,建议使用监控工具(如 TensorBoardW&B),实时监控损失曲线、训练进度等信息,以便及早发现潜在问题,例如模型过拟合或收敛不良。
    • 验证集评估
      • 在训练的过程中,定期使用验证集来评估模型在未见过的数据上的表现。验证集的设计应当与训练集保持一致,且最好覆盖多个任务场景,以确保模型的泛化能力。
  5. 模型评估与测试

    • 评估模型性能
      • 一旦模型完成微调,需要对其在多个任务上的性能进行评估。常用的评估指标包括准确率(Accuracy)、精确率(Precision)、召回率(Recall)、F1 分数等,具体选择取决于任务的性质。
    • 测试实际应用场景
      • 为了确保模型在真实场景中可以有效工作,可以通过实际的应用场景进行测试。比如,你可以通过模拟用户指令来测试模型在不同任务上的反应是否符合预期。
  6. 部署与应用

    • 模型导出与部署
      • 完成微调后,模型可以导出为可部署的格式(例如 TorchScriptONNX 等),并部署到生产环境中。
    • 持续优化与反馈
      • 在生产环境中,模型的表现会收到用户交互的影响,因此可以根据用户反馈进行进一步的微调和优化。

不使用明确的 instruction 会让模型在推理阶段更依赖上下文信息,而不是等待明确的指令提示。它可以使模型在连续对话生成任务中表现得更自然,但缺点是,模型可能在多任务环境中表现不如明确指令下那么灵活,因为它没有被明确告知应该做什么任务。

知识灌注(Knowledge Infusion)指的是将特定的知识明确灌输到模型中,在指令微调阶段也是可以进行知识灌注的,使得模型能够记住特定信息,并在任务中使用这些知识。例如,你想让模型知道自己的名字,或掌握一些特定领域的事实,这种知识可以在指令微调阶段注入。

知识灌注的可行性:

可以进行知识灌注:在指令微调阶段,确实可以通过有针对性的训练数据进行知识灌注。你可以提供包含这些知识的训练数据,通过上下文或明确指令提示模型,从而让模型学会特定的知识。例如,你可以在对话数据中加入这样的对话:

用户:你叫什么名字? 模型:我是 MateConv,一个智能对话助手。 通过这样的训练数据,模型会逐渐记住这些知识,并在推理时能够自发地根据这些提示进行正确的回答。


全量指令微调和微调之间的区别和联系

  这其实是一个非常综合且重要的问题,尤其是当我们在大规模模型微调的场景下,追求平衡性能资源消耗时。全量指令微调高效微调(也叫参数高效微调)各自代表了不同的微调策略,它们有不同的应用场景和技术优点,但也存在一些相互联系。接下来我们将详细对比它们的区别联系

一、全量指令微调(Full Instruction Tuning)

全量指令微调指的是对整个模型的所有参数进行微调。这种方法最常用于当你有充足的数据和计算资源,并且希望模型在一个特定任务或多个任务上达到最佳性能。

特点:
  1. 微调所有模型参数

    • 全量微调意味着所有的预训练参数都会进行调整。模型在新的指令任务上逐渐适应并优化所有权重,从而能够高效地处理特定任务。
  2. 适用于大规模数据和明确任务

    • 适合处理大量的数据集,并且能通过多任务、多领域的指令数据微调模型。这种方法能够让模型最大限度地适应新的任务需求。
  3. 计算和存储开销大

    • 由于模型的所有参数都会被更新,全量指令微调通常需要非常高的计算资源和存储空间,尤其是当你处理的是数十亿参数规模的大模型时。
  4. 微调后效果精确

    • 全量微调可以保证模型在微调后的任务上有最好的表现,模型完全适应了新的指令数据,在目标任务上能够达到最高精度。
  5. 应用场景

    • 通常应用在有足够计算资源的环境中,特别是需要极高性能、处理特定任务(如法律文本分析、医疗诊断等)的模型开发。
优点:
  • 高性能,任务适应能力强。
  • 精确控制模型行为和生成效果。
缺点:
  • 计算资源和时间开销巨大。
  • 训练耗费大量的显存和存储资源,部署和更新较为昂贵。

二、高效微调(Parameter-Efficient Tuning)

高效微调(Parameter-Efficient Tuning)是一种在不更新全部参数的情况下,选择性地对模型某些部分进行微调的策略。常见的高效微调技术包括 LoRA(Low-Rank Adaptation)Prefix TuningAdapter Tuning 等。

特点:
  1. 微调部分参数

    • 高效微调通过引入一些小型模块(如 Adapter 层、前缀)或调整模型的某些子结构(如低秩矩阵),只对这些部分进行微调,而不调整整个模型的所有权重。
  2. 减少显存和计算需求

    • 因为只需要更新少量参数,高效微调显著降低了显存消耗和计算资源的需求。这使得即使在资源受限的环境下,也能微调大规模的预训练模型。
  3. 适用于低资源环境

    • 高效微调非常适合数据集较小、计算资源有限的场景。例如,在嵌入式系统或边缘计算设备上部署大模型时,高效微调可以让模型在这些环境中高效运行。
  4. 模块化和可重用性

    • 高效微调的另一个优点是其模块化。微调过程中引入的小型模块可以在不同的任务之间进行共享或转移,避免重复训练。例如,适用于某个领域的 Adapter 层可以在该领域的多个任务上重复使用。
  5. 微调后效果良好但不如全量微调

    • 虽然高效微调可以达到很好的效果,但与全量指令微调相比,性能上略逊一筹。尤其是在对任务精度要求极高的场景下,可能需要全量微调来最大化模型的效果。
优点:
  • 计算资源、显存开销小,适合低资源环境。
  • 训练效率高,适用于快速迭代和部署。
缺点:
  • 在特定高精度任务上,性能可能不如全量微调。
  • 微调时引入的模块或层会增加模型的复杂度,需要额外管理。

三、区别

  1. 参数调整范围

    • 全量指令微调:微调所有模型参数,更新整个网络的权重。
    • 高效微调:只微调模型中的部分参数或增加的模块,如 LoRA、Adapter 层,原有的大部分权重保持不变。
  2. 计算和显存需求

    • 全量指令微调:显著增加计算和显存需求,尤其是对于数十亿参数规模的模型,要求非常高。
    • 高效微调:显存需求低,适合在资源有限的环境中运行。只需要少量的参数更新,因此计算资源消耗更低。
  3. 性能表现

    • 全量指令微调:在任务上的表现往往最佳,因为模型所有参数都被精细优化,可以最大化适应任务需求。
    • 高效微调:性能表现虽然不错,但通常不及全量微调,尤其是在一些复杂或精细的任务上。
  4. 部署和模块化

    • 全量指令微调:整个模型被微调,每次微调都需要重新部署整个模型,更新较为繁琐。
    • 高效微调:只需要部署微调后的模块,模型的主体部分保持不变,方便快速迭代和更新。
  5. 训练时间

    • 全量指令微调:训练时间较长,特别是在大规模模型上,因为需要更新大量的参数。
    • 高效微调:训练时间相对较短,因为只需要微调少部分参数。

四、联系

  1. 相同的目标

    • 无论是全量指令微调还是高效微调,它们的最终目标都是让预训练模型适应特定任务,提高在目标任务上的表现。两者的区别更多在于实现的方式和资源的权衡。
  2. 可以结合使用

    • 在某些场景下,可以结合全量指令微调和高效微调。例如,你可以先对整个模型进行全量指令微调,以确保模型的高精度表现,然后在低资源环境中应用高效微调以进一步优化模型,或者扩展到新任务中而不需要微调整个模型。
  3. 知识共享

    • 高效微调中的一些方法(如 Adapter 模块)可以在不同任务之间共享,这些模块可以通过全量微调的模型基础上进一步进行高效微调,扩展到不同的任务领域。

总结

  • 全量指令微调是一个“昂贵”的但高效的微调方法,适合资源充足、需要高精度表现的场景,尤其是那些需要处理多个复杂任务的场景。
  • 高效微调则是一个“轻量级”的方法,适合资源有限的场景,并且在需要快速部署或频繁更新时非常有用。它可以通过少量的参数更新,实现较好的模型性能。

五、单轮对话和多轮对话指令微调的区别

1. 上下文处理
  • 单轮对话微调

    • 模型只需要处理当前输入和输出之间的映射,不需要考虑上下文,也不需要保存或记住之前的对话内容。
    • 微调数据可以是一些独立的指令,比如“生成一个回复”、“回答这个问题”等。
    • 微调时,数据集中每个问题和回答都可以视作独立的样本,因此微调过程相对简单。
  • 多轮对话微调

    • 模型需要在每一轮对话中跟踪和理解完整的对话历史,包括之前的问答信息。模型必须具备记忆和状态管理的能力。
    • 微调时,数据集不仅需要包含每轮对话的输入和输出,还需要保留对话的上下文,使模型能够在生成回复时参考前文。
    • 这类任务的微调更加复杂,因为模型需要学会如何在多个回合中保持对话的连贯性,避免前后矛盾。
2. 任务复杂性
  • 单轮对话微调

    • 任务相对简单,通常用于一些固定回答的任务,例如问答系统、知识查询、单轮任务执行等。模型的输出只需对当前输入负责。
    • 微调时,模型不需要处理复杂的对话逻辑,只需对每个输入生成最合适的响应。
  • 多轮对话微调

    • 任务复杂度更高,尤其是在多回合对话中,模型需要处理上下文依赖、对话状态跟踪等问题。
    • 微调时,数据集需要涵盖不同类型的对话情境,模型不仅要能生成单轮的合适回复,还要保证整个对话的流畅性和一致性。
3. 模型训练需求
  • 单轮对话微调

    • 对训练数据的需求相对较少,数据集只需要包含独立的问题和回答。由于没有上下文依赖,数据样本的数量可以较小。
    • 模型的生成质量主要取决于它在每次输入时生成单一答案的能力。
  • 多轮对话微调

    • 多轮对话需要更丰富的训练数据,涵盖多种对话情境和长短不一的对话历史,以帮助模型学习如何处理复杂的上下文。
    • 模型需要学习如何在多个回合中保持对话的上下文一致性,因此训练时间和资源需求更高。

  • 两种主流的全量指令微调方法

  从模型训练角度来说,指令微调远不如预训练复杂,在实际进行指令微调的过程中,可以选择使用transformer库的原生方法实现全量指令微调,也可以选择一些开源的项目如Llama-Factory进行快速指令微调。这里我们主要尝试使用transformer库的原生方法来执行全量指令微调。

  • 指令微调数据集

  不同于预训练数据集,需要海量、高质量、多样性的文本数据,才能训练得到一个流畅问答的大模型,全量指令微调对于数据集的要求相对较低,制作门槛也相对较低,其中Llama-Factory上有非常完整的目前可用于中英文指令微调的数据集:https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md#%E6%95%B0%E6%8D%AE%E9%9B%86

image-20241024170251722

这里我们选取数据规模相对较小的匠数科技的sft数据集。该数据集是一个是一个完整、格式统一、安全的大模型训练和研究资源。从网络上的公开数据源收集并整理了大量开源数据集,对其进行了格式统一,数据清洗,包含10M条数据的中文数据集和包含2M条数据的英文数据集。总量大约在3B token,适合小尺寸中文大语言模型进行指令微调:

image-20241024170316690

匠数科技大模型sft数据集官方地址:https://www.modelscope.cn/datasets/deepctrl/deepctrl-sft-data

一、借助transformer原生方法完成指令指令微调

1.指令微调数据集获取与数据集清洗

  • 借助魔搭社区下载数据集

FENCE0

image-20241024171058316

FENCE0

image-20241024171518912

下载完成后即可在dataset文件夹内看到该文件:

image-20241024171610385

网盘地址如下:

image-20250109181513930
  • 查看指令微调的数据文件
import jsonlines

# 查看JSONL文件的前几行
file_path = './dataset/sft_data_zh.jsonl'

with jsonlines.open(file_path) as reader:
for i, obj in enumerate(reader):
print(obj) # 打印每一行
if i >= 4: # 只打印前5行
break

其中每条数据的数据格式如下:

image-20241024171728639

而其中我们只需要提取对话的信息即可:将符合条件的部分保存到一个 CSV 文件中。整个过程涉及到数据的筛选、清洗、处理和写入。具体步骤如下:

  1. 选择输入文件

    • 根据参数 contain_history,确定要处理的数据集文件名为 'sft_data.csv''sft_data_single.csv'
  2. 文本筛选函数

    • chinese_ratio(text):计算一个文本中中文字符的比例,用来筛选是否大部分内容是中文。
  3. 处理数据并写入 CSV

    • process_and_write_data(data):对数据进行筛选,检查每一条对话是否符合以下条件:
      • 如果 contain_history 为真,则数据必须有对话历史。
      • 对话的问句 (q) 和答句 (a) 都必须存在,并且它们的长度要在指定范围内(问句长度在 10 到 256 之间,答句长度在 5 到 256 之间)。
      • 问句和答句的中文字符比例必须大于 90%。
    • 如果数据符合条件,则将问句、答句和对话历史(如果有的话)添加到列表中,并写入 CSV 文件。
  4. 逐行读取 JSONL 数据集

    • sft_datasets 包含了一个要处理的 JSONL 数据集文件路径。
    • 使用 jsonlines 库逐行读取数据,并将问答对保存在 data 列表中。每当 data 列表中累计了 1000 条记录(chunk_size),就将数据进行处理并写入到 CSV 文件中。
    • 如果读取的行有格式问题,代码会跳过并继续处理下一行。
  5. 进度条

    • 使用 tqdm 库为整个处理过程添加进度条,显示处理数据的进度,并在每处理 chunk_size 条数据后更新进度。
  6. 输出结果

    • 生成的 CSV 文件包括三个列:history(对话历史)、q(问句)、a(答句)。
import csv
import itertools
import re
import json
import jsonlines
import psutil
import ujson
import numpy as np
import pandas as pd
from transformers import AutoTokenizer
from datasets import load_dataset
from tqdm import tqdm
bos_token = "<s>"
eos_token = "</s>"
tokenizer = AutoTokenizer.from_pretrained('./model/miniDeepSeek_tokenizer', use_fast=False)
print('tokenizer词表大小:', len(tokenizer))
from tqdm import tqdm # 导入 tqdm

def sft_process(contain_history=False):
file_name = 'sft_data.csv' if contain_history else 'sft_data_single.csv'

def chinese_ratio(text):
"""计算文本中中文字符的比例。"""
chinese_chars = re.findall(r'[\u4e00-\u9fff]', text) # 匹配中文字符
return len(chinese_chars) / len(text) if text else 0

def process_and_write_data(data):
"""处理数据并将其写入 CSV 文件。"""
q_lst, a_lst, history_lst = [], [], []
for per in data:
history, q, a = per['history'], per['q'], per['a']

# 数据筛选条件
if (contain_history and not history) or not q or not a:
continue
if len(q) < 10 or len(a) < 5:
continue
if len(q) > 256 or len(a) > 256:
continue
if not (chinese_ratio(q) > 0.9 and chinese_ratio(a) > 0.9):
continue

# 将有效数据添加到列表
q_lst.append(q)
a_lst.append(a)
if contain_history:
history_lst.append(history)
else:
history_lst.append([])

# 创建 DataFrame 并写入 CSV 文件
df = pd.DataFrame({'history': history_lst, 'q': q_lst, 'a': a_lst})
df.to_csv(f'./dataset/{file_name}', mode='a', header=False, index=False,
lineterminator='\r\n', escapechar='\\', quoting=csv.QUOTE_MINIMAL)

chunk_size = 1000 # 每次处理的记录数
data = []

# 创建 CSV 文件并写入表头
with open(f'./dataset/{file_name}', 'w', encoding='utf-8') as f:
f.write('history,q,a\n')

sft_datasets = ['./dataset/sft_data_zh.jsonl']

# 开始处理数据集
for path in sft_datasets:
with jsonlines.open(path) as reader:
total_lines = sum(1 for _ in open(path)) # 获取总行数

# 使用 tqdm 创建进度条
with tqdm(total=total_lines, desc="Processing lines") as pbar:
for idx, obj in enumerate(reader):
try:
data.append({
'history': obj.get('history', ''),
'q': obj.get('input', '') + obj.get('q', ''),
'a': obj.get('output', '') + obj.get('a', '')
})

# 每达到 chunk_size,就处理并写入数据
if len(data) >= chunk_size:
process_and_write_data(data)
data = []
pbar.update(chunk_size) # 更新进度条

except jsonlines.InvalidLineError as e:
print(f"Skipping invalid JSON line {idx + 1}: {e}")
continue

# 处理剩余的数据
if data:
process_and_write_data(data)
pbar.update(len(data)) # 更新进度条

print("数据处理完成!")
sft_process(contain_history=False)

查看处理后的数据文件:

import pandas as pd

# 加载 CSV 文件
file_path = './dataset/sft_data_single.csv' # 确保路径正确

# 使用 pandas 读取前 5 行
df = pd.read_csv(file_path, nrows=5)

# 显示前 5 行数据
print(df)
image-20250109181612370

2.指令微调代码编写与代码解释

  • 指令微调代码

  在准备好数据集之后,接下来即可开始指令微调。首先我们需要编写一个指令微调的脚本文件full_sft.py其中代码如下:

FENCE0

  • 指令微调代码解释

接下来,我们分部分解释代码的主要功能和逻辑:

1. 导入依赖库

FENCE0

  • 这部分导入了常见的库,如 ostimepandas,以及用于深度学习训练的 PyTorch 库。
  • 主要导入了用于加载分词器和模型的 transformers 库(Hugging Face 的工具),以及自定义的 Transformer 模型、配置文件 LMConfig 和数据集处理模块 SFTDataset

2. 日志记录函数 Logger

FENCE1

  • 功能:这个函数用于在分布式训练中,只在主进程上输出日志内容(因为在分布式环境中,可能会有多个进程执行同样的操作)。
  • dist.get_rank():返回当前进程的 rank,只有 rank 为 0 的进程(主节点)才会输出日志。

3. 学习率调度函数 get_lr

FENCE2

  • 功能:实现余弦退火学习率调度(Cosine Annealing Learning Rate)。在训练的早期,学习率先逐步升高(warmup),然后在训练后期逐步减小。
  • 这种调度策略有助于模型在训练中快速收敛,同时避免后期训练时步长过大导致不稳定。

4. 模型训练函数 train_epoch

FENCE3

  • 核心功能:执行模型的单轮训练,计算损失并更新模型参数。
  • 重要步骤
    • 学习率更新:通过 get_lr 动态调整学习率。
    • 损失计算:使用 cross_entropy 损失函数,忽略 ignore_index=0(常用于忽略填充的 token)。
    • 梯度累积:通过 args.accumulation_steps 控制梯度累积,适用于较大模型的训练。
    • 梯度裁剪:使用 clip_grad_norm_ 限制梯度的最大范数,防止梯度爆炸。
    • 保存检查点:定期保存模型权重到 .pth 文件中,支持恢复训练。

5. 模型初始化 init_model

FENCE4

  • 功能:加载模型和分词器,支持从不同源加载模型(如自定义模型权重或 Hugging Face 的 AutoModel)。
  • 模型权重加载:从 ./out 目录中加载预训练权重(.pth 文件),并加载到 Transformer 模型中。
  • 参数统计:计算模型的总参数量并输出。

6. 分布式训练初始化 init_distributed_mode

FENCE5

  • 功能:初始化分布式训练环境,使用 NCCL 后端进行 GPU 通信。适用于多 GPU 或多节点训练。

7. 主流程

FENCE6

  • 命令行参数解析:使用 argparse 解析训练相关参数(如学习率、

批次大小等)。

  • 数据加载:使用 SFTDataset 处理数据集,DataLoader 负责将数据批量化用于训练。

  • 自动混合精度训练:通过 torch.cuda.amp.GradScaler 实现半精度训练(如 float16),以减少显存占用。

  • 训练循环:遍历训练的 epoch,调用 train_epoch 进行模型更新。

  • 代码保存

编写完代码后,将其保存在项目主目录下:

1736417846860

3.全量指令微调训练流程

  • 开启全量指令微调

需要注意的是,当前脚本是从固定路径下读取质量微调数据,所以在开始脚本之前,要再次确认指令微调数据是否已经创建完成:

image-20241024183045918

FENCE0

image-20241024182213662

并且可以在wandb中查看训练效果:

image-20241024182121539
  • 中断后继续训练

  和预训练一样,全量微调的代码也是支持中断后继续训练的,并且和预训练一样,默认情况下是训练1000步保存一次:

image-20241024180457524

同时会保存模型参数如下:

1736417915372

此外,也可以继续使用上一小节介绍的screen工具来持久化会话。

  • 训练结束
9f1b2321dc79550b1e0f194e165ff22

二、模型对话效果测试

  在完成了模型指令微调之后,接下来即可测试模型对话效果。

  • 对话测试
import torch
import random
import numpy as np
from transformers import AutoTokenizer
from model.model import Transformer
from model.LMConfig import LMConfig
# 1. 设置设备和随机种子
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def setup_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

setup_seed(1337)
  • 目的:设置随机数种子是为了确保模型运行时的确定性和复现性
    • random.seednp.random.seedtorch.manual_seed 用于固定 Python、NumPy 和 PyTorch 的随机数生成器。
    • 设备选择:优先选择 GPU 进行推理。如果 GPU 不可用,则使用 CPU。
    • cudnn.deterministic = True 确保在使用 CUDA 加速时,结果可以复现,但会略微降低性能。
# 2. 初始化模型和分词器
lm_config = LMConfig()
lm_config

这里确保"dim": 768或者512,才可以使用对应参数的模型。

max_seq_len = 1024 # 可以根据需要调整
lm_config.max_seq_len = max_seq_len

model = Transformer(lm_config).to(device)
model_path = './out/full_sft_512_moe.pth' # 替换为你的模型路径
state_dict = torch.load(model_path, map_location=device)
model.load_state_dict(state_dict)
model.eval()
tokenizer = AutoTokenizer.from_pretrained('./model/miniDeepSeek_tokenizer')
  • 模型加载
    • LMConfig:初始化模型配置,例如设置 max_seq_len 表示模型的最大输入序列长度。
    • 加载模型权重:从指定的 model_path 路径加载已经微调好的模型权重 (full_sft_512.pth)。
    • 模型评估模式model.eval() 切换模型为评估模式,禁用 dropout 和 batch normalization 的训练行为。
  • 分词器加载
    • 分词器用于将输入的自然语言文本转换为模型所需的 token 序列,并可以将模型输出的 token 序列转回人类可读的文本。这里从 mateconv_tokenizer 路径加载分词器。
# 3. 对话函数:生成完整回复
def generate_reply(prompt, temperature=0.5, top_k=16, stream=True):
messages = [{"role": "user", "content": prompt}]

# 使用自定义的 prompt 模板 (根据你的应用逻辑)
new_prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)[-(max_seq_len - 1):]

input_ids = tokenizer(new_prompt).data['input_ids']
input_ids = torch.tensor(input_ids, dtype=torch.long, device=device).unsqueeze(0)

generated_text = ""
with torch.no_grad():
# 生成器返回的生成结果
res_y = model.generate(input_ids,
tokenizer.eos_token_id,
max_new_tokens=max_seq_len,
temperature=temperature,
top_k=top_k,
stream=stream)

# 从生成器逐步获取生成结果
try:
y = next(res_y)
except StopIteration:
print("No answer")
return ""

history_idx = 0
while y is not None:
answer = tokenizer.decode(y[0].tolist())
if answer and answer[-1] == '�':
try:
y = next(res_y)
except StopIteration:
break
continue

if len(answer):
generated_text += answer[history_idx:]

try:
y = next(res_y)
except StopIteration:
break
history_idx = len(answer)

return generated_text
  • 功能:这个函数用于生成对话回复,流程如下:
    1. 自定义 prompt:将用户输入(prompt)转换为自定义格式,这里使用了 apply_chat_template 生成对话的模板,将 messages 处理为可供模型生成的输入。
    2. 生成 input_ids:将 new_prompt 转换为 token 序列 (input_ids) 供模型使用,并且调整其形状以适应批处理(unsqueeze(0) 增加批次维度)。
    3. 模型生成:通过 model.generate 函数,使用贪心搜索或随机采样(受 temperaturetop_k 控制)生成回复。
      • temperature 控制生成时的随机性,值越小生成结果越确定,越大则越随机。
      • top_k 限制采样的选择范围,取概率最高的 k 个 token。
    4. 逐步解码:模型生成结果会被逐步解码为可读文本,直到完成生成或遇到 eos_token
    5. 返回结果:最终返回生成的完整回复文本。
参数说明:
  • prompt:用户输入的对话文本,模型将根据该输入生成回复。
  • temperature:采样时的随机性控制参数,较高的值会增加生成内容的多样性,较低的值会生成更确定的输出。
  • top_k:采样时只从概率最高的前 k 个 token 中选择,限制生成的词汇选择范围,避免生成低概率的单词。
  • stream:决定是否以流式方式逐步获取生成结果。

这里需要注意的是,大模型判断回复停止的主要依据是 eos_token(End of Sequence token),即序列结束标记。eos_token 是用于告诉模型生成任务已经完成的特殊标记。模型在生成文本时,一旦生成了 eos_token,它就知道应该停止输出。


补充介绍序列结束标记预测原理

大模型如何判断下一个单词(token)是 eos_token(序列结束标记),实际上是通过模型的生成机制,结合模型在预训练和指令微调阶段学到的语言模式来决定的。模型通过概率分布来选择下一个 token,而这个概率分布是在预训练指令微调中学到的。

1. 生成机制的工作原理

模型生成文本的过程通常基于自回归机制,也就是模型在生成每个 token 时,都会基于之前生成的所有 token 来预测下一个 token。它并不会明确知道下一个 token 是什么,而是通过计算每个可能 token 的概率分布,从中选择最可能的 token(包括 eos_token)。

步骤概述:
  1. 输入上下文:给定一个输入(如用户问题或对话),模型会根据上下文生成一个概率分布,这个分布表示每个可能的 token 出现在该位置的概率。
  2. 选择下一个 token:模型通过贪心搜索、随机采样(基于 temperature)或其他策略,选择下一个 token。eos_token 作为候选之一,它的概率也由模型预测出来。
  3. 生成停止:当模型预测出 eos_token 并选择它作为下一个 token 时,生成过程停止。

2. 影响 eos_token 判断的因素

(1) 预训练的影响

预训练的过程中,模型会接触到大量的自然语言文本。通过自回归语言模型的方式,模型学会了如何生成自然语言,包括如何适时结束一段句子或文本。因此,预训练赋予了模型关于自然语言的结构、语法、句法模式的基础知识。

  • 预训练阶段的 eos_token:在预训练的文本数据中,通常会在每个训练样本的末尾加上 eos_token,因此模型在预训练中学会了何时结束一段文本。在生成过程中,模型会根据上下文推测是否应该结束文本。
(2) 指令微调的影响

指令微调专门针对任务的要求进行了微调,模型不仅学习了如何生成回答,还学习了如何在不同的任务和上下文中生成适合的结束标记。因此,指令微调对 eos_token 的判断进一步进行了优化,使得模型在实际任务中能够更好地判断何时结束输出。

  • 指令微调阶段的 eos_token:指令微调阶段的训练数据中,通常会标注任务的起始和结束,并提供明确的输入和输出。这使得模型能更好地理解在特定任务(如对话、问答等)中,何时应该生成 eos_token 来结束对话或回答。

3. 模型如何具体判断 eos_token

在每个生成步骤中,模型会基于已经生成的 token 来预测下一个 token。预测的过程通常如下:

  1. 概率分布计算:模型通过计算输入 token 序列的上下文信息,输出一个表示所有可能 token 的概率分布(Softmax)。这个概率分布表示每个 token 出现的可能性,包括 eos_token

    例如,假设模型在某个位置预测下一 token 时,给出了以下概率分布:

    • the: 0.4
    • a: 0.3
    • eos_token: 0.2
    • 其他 token:0.1
  2. 选择 token:根据生成策略(如贪心搜索、随机采样等),模型会选择概率最大的 token,或者根据设定的 temperaturetop_k 进行采样。如果 eos_token 的概率最高或者通过采样选中,模型会生成 eos_token,表示生成过程结束。

    • 贪心搜索:选择当前概率最大的 token。
    • 随机采样:根据概率进行采样,temperature 控制采样的随机性。
    • top-k 采样:只从概率最高的前 k 个 token 中选择。

  • 测试问答
response = generate_reply("长江、")
response
response = generate_reply("你好,好久不见!")
response
response = generate_reply("请问什么是机器学习?")
response
response = generate_reply("请问机器学习和深度学习的区别是什么?")
response

三、DPO强化学习微调过程

1.DPO强化学习基本介绍

  DPO强化学习微调(Demonstration-Based Policy Optimization) 是一种基于示范学习的强化学习方法,旨在通过利用人工提供的示范数据来引导模型的策略优化过程。这种方法通常用于语言模型和其他生成任务的训练,以便在较少的标注数据或有限的奖励信号的情况下进行有效的微调。

DPO的基本概念

  DPO强化学习微调的核心思想是:通过模仿专家提供的示范,来优化模型的决策策略。与传统的强化学习方法相比,DPO不依赖于直接的环境反馈(如奖励函数),而是通过引导模型逐步接近专家行为的方式进行优化。具体而言,DPO利用示范数据来构建一个参考模型,然后在训练过程中通过强化学习的算法(如策略梯度方法)优化目标模型,使其能够生成与专家示范相似的输出。

DPO的工作原理
  1. 示范数据集:首先,需要收集一些专家的示范数据,这些数据可以是手工编写的文本、已标注的样本,或从其他高质量来源获取的样本。这些示范数据为模型提供了一个优化的“目标”,即模型需要学习模仿的行为。

  2. 策略优化:在有了示范数据后,DPO通过强化学习的策略优化方法来训练目标模型。与传统的强化学习不同,DPO的奖励函数通常是通过与参考模型的输出差异来计算的,这意味着优化目标是使目标模型的输出尽量接近参考模型的行为。

  3. 逐步调整和微调:在DPO训练过程中,模型通过不断调整策略,增强其与专家示范的相似度。通过这种方式,模型不仅能模仿专家行为,还能在一定程度上自主发现并优化生成过程中的策略,逐渐提升生成质量。

  4. 适应性学习:DPO的另一个优势是其在训练过程中具有较强的适应性。随着训练的深入,模型的策略将逐渐从简单的模仿转向更为复杂的自我强化和优化,最终能够在多种任务中提供有效的输出。

DPO与传统强化学习的比较
  • 奖励信号:传统的强化学习依赖环境提供的即时奖励信号来引导学习,而DPO则通过专家示范作为学习信号,相对减少了对环境奖励的依赖。
  • 数据效率:DPO通常比传统强化学习方法数据效率更高,尤其在缺乏丰富的环境反馈时,示范数据能够有效引导模型学习。
  • 应用场景:DPO特别适用于那些难以设计明确奖励函数的任务,例如自然语言生成、文本生成和对话系统等。

  DPO强化学习微调在多个领域得到了广泛应用,尤其是在自然语言处理(NLP)和生成式模型的微调上。例如,使用DPO方法微调生成式语言模型(如GPT系列模型),可以使其生成更符合人类专家示范的高质量文本。

  DPO强化学习微调通过利用专家示范数据,有效提升了模型的生成能力,并且在数据稀缺的情况下,依然能够实现高效的策略优化。这种方法为复杂任务的模型训练提供了一个新颖而高效的解决方案,特别是在强化学习和自然语言生成领域。

而OpenAI在其2024年年终系列发布会上,也重点介绍了未来将上线的在线DPO微调功能:

image-20250109185054365

2.DPO数据集介绍

import json

# 打开并读取 JSON 文件
with open('./dataset/dpo/train_data.json', 'r') as f:
data = json.load(f)

print(data[:1]) # 打印前 5 条记录
data[:1][0].keys()

该数据集为强化学习微调任务中的一条训练样本,它的结构是一个字典,包含了几个字段,每个字段代表着特定的信息。以下是字段的详细解释:

  1. chosen:

    • 这个字段包含了一个完整的答案或者示范,描述了一个特定的问题的解答。它通常是强化学习中“正确”或者“理想”答案的部分。在这个例子中,chosen字段提供了一段Python代码,说明了如何计算推文的政党边际概率,以及如何标记是否为国会议员转发的推文。
    • 内容示例
      tweets['party'] = tweets['party'].replace(0, 'Democrat')
      tweets['party'] = tweets['party'].replace(1, 'Republican')
      tweets['party'] = tweets['party'].replace(2, 'Independent')

      party_counts = tweets.groupby('party').size().sort_values(ascending=False)

      party_marg = party_counts / party_counts.sum()

      print("政党的边际概率:")
      print(party_marg)
      这段代码演示了如何将政党的编号(0, 1, 2)转换为政党名称,并计算每个政党的边际概率。
  2. rejected:

    • 这个字段包含了被拒绝或者不符合预期的解答。它展示了一个较为基础或不完全的答案。在这个例子中,rejected字段提供了另一种方法来计算政党的边际概率,但相比chosen的答案,它少了一些处理细节,显得不够完善。
    • 内容示例
      party_count = tweets.groupby('party').count()
      party_count.loc['Democrats', 'retweet_count'] / party_count.loc['Republicans', 'retweet_count']
  3. id:

    • 这是该条数据的唯一标识符。在这个例子中,它的值为0,即该条数据的ID。
  4. prompt:

    • 这是提供给模型的问题或任务描述。在这个例子中,prompt字段描述了关于美国国会转发推文的任务,包括有关如何计算政党边际概率和如何标记国会议员转发的问题。这个问题会引导模型生成相关的答案。
    • 内容示例: 任务描述是关于美国国会的转发数据,包含有关政党、国会议员转发等问题。

总之这条数据是一个强化学习微调任务的数据样本。每条数据包含三个主要部分:

  • chosen:模型生成的“理想”答案;
  • rejected:模型生成的“不太理想”或者不符合预期的答案;
  • prompt:提供给模型的任务描述或问题。

这些数据用于训练模型,使得它能够在给定类似任务时生成更合适、更符合预期的答案。通过对比chosenrejected字段,模型学习如何改进其生成的回答。

3.DPO强化学习微调过程

注:公开课中DPO强化学习微调直接使用hugging face的trl库进行微调,需要单独设置模型格式才能使用。完整版课程中会介绍手写DPO算法实现流程,同时将详细介绍hugging face模型格式。公开课时间有限,此处直接掠过。大家可以直接使用强化学习微调后的模型直接使用。

  • 创建满足hugging face模型结构的模型文件:
image-20250109190256194

该文件内容在网盘地址如图所示:

image-20250109190939681
  • 创建DPO微调脚本

在主目录下创建名为dpo_train.py的脚本,内容如下:

FENCE0

这段代码主要用于设置并启动一个基于 miniDeepSeek 模型的训练过程。它涉及到模型初始化、数据加载、训练配置和训练过程的启动。以下是逐行的详细分析:

导入库和环境配置

FENCE0

  • os: 用于与操作系统交互。在这里,用来指定使用的 GPU(CUDA_VISIBLE_DEVICES)。
  • warnings.filterwarnings('ignore'): 禁用警告信息的显示,避免在训练过程中输出冗余的警告。
  • CUDA_VISIBLE_DEVICES = '0': 设定仅使用编号为 0 的 GPU。
  • transformers: 来自 Hugging Face 的 transformers 库,用于加载模型和分词器。
  • trl: 来自 trl(强化学习)库的 DPOConfigDPOTrainer,用于强化学习训练设置和执行。DPO(Demonstration-Based Policy Optimization)用于强化学习中的策略优化。
  • datasets: 用于加载训练数据集。
init_model 函数

FENCE1

  • 该函数的作用是初始化模型和分词器,并将模型移至 GPU。
  • model_name_or_path: 模型路径,指向本地存储的 miniDeepSeek 模型。
  • tokenizer_name_or_path: 分词器路径,指向本地存储的 miniDeepSeek 分词器。
  • AutoModelForCausalLM.from_pretrained(...): 从指定路径加载预训练的 causal LM(自回归语言模型)模型。
  • AutoTokenizer.from_pretrained(...): 从指定路径加载分词器。use_fast=False 表示不使用快速版本的分词器(适用于性能较低的环境或兼容性问题)。
  • tokenizer.pad_token = tokenizer.eos_token: 设置分词器的 pad token 为 eos token。
  • model = model.to(device): 将模型移至 cuda:0 GPU。
  • 函数最终返回初始化的模型和分词器。
main 逻辑

FENCE2

  • 这是程序的主入口,调用 init_model() 函数来初始化模型和分词器。
DPO 配置和训练准备

FENCE3

  • 这里使用 DPOConfig 来设置训练过程的超参数。
    • per_device_train_batch_size=1: 每个设备的训练批次大小为 1。
    • remove_unused_columns=False: 是否删除数据集中的未使用列。
    • report_to="none": 禁用任何报告机制(例如,TensorBoard、Weights & Biases等)。
    • save_steps=1000: 每经过 1000 步训练就保存一次模型。
    • learning_rate=4e-5: 设置学习率为 4e-5。
    • output_dir="./dpo_output": 设置训练结果输出路径。
数据集加载

FENCE4

  • 使用 load_dataset 从指定的 JSON 文件中加载训练数据。train_data.json 是训练数据集的路径。
初始化 DPO Trainer

FENCE5

  • DPOTrainer 用于强化学习训练。
    • model: 传入初始化的 miniDeepSeek 模型。
    • ref_model=None: 这里没有提供参考模型。
    • args=training_config: 传入之前定义的训练配置。
    • beta=0.1: 训练中的超参数,用于平衡策略优化过程中的奖励和损失。
    • train_dataset=train_dataset['train']: 传入训练数据集。
    • tokenizer=tokenizer: 使用的分词器。
    • max_length=512: 设置输入序列的最大长度。
    • max_prompt_length=512: 设置提示词的最大长度。
启动训练

FENCE6

  • 最后,调用 train() 方法开始训练过程。

然后输入python dpo_train.py开始执行脚本:

image-20250109184423843

约1各半小时左右运行完毕。可以在项目主目录的dpo_output文件夹中看到最终DPO微调结果:

1736421062064

可见网盘中的./out/rl_768.pth就是DPO微调后的模型权重,可以直接下载并使用。

image-20250109191237496

接下来测试模型对话效果。

import torch
import random
import numpy as np
from transformers import AutoTokenizer
from miniDeepSeek.model import Transformer
from miniDeepSeek.LMConfig import LMConfig
# 1. 设置设备和随机种子
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def setup_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

setup_seed(1337)
# 2. 初始化模型和分词器
lm_config = LMConfig()
lm_config
max_seq_len = 512 # 可以根据需要调整
lm_config.max_seq_len = max_seq_len

model = Transformer(lm_config).to(device)
model_path = './out/rl_768.pth' # 替换为你的模型路径
state_dict = torch.load(model_path, map_location=device)
model.load_state_dict(state_dict, strict=False)
model.eval()
tokenizer = AutoTokenizer.from_pretrained('./model/miniDeepSeek_tokenizer')
  • 测试问答
response = generate_reply("长江、")
response
response = generate_reply("你好,好久不见!")
response
response = generate_reply("请问什么是机器学习?")
response
response = generate_reply("请问机器学习和深度学习的区别是什么?")
response

四、调用前端进行对话

  • 创建基于前端的聊天机器人

  将web_chat放在项目主目录下:

image-20241024195920335

然后在命令行中运行:

FENCE0

FENCE0

image-20241024200002454

若未安装streamlit,则需要使用如下指令进行安装

pip install streamlit

然后将8501映射到本地:

image-20241024200139189

然后打开浏览器,即可在8501端口进行对话:

image-20250109192504914